sp_block_participation/
lib.rs

1//! Crate providing block participation data through the inherent data mechanism.
2//!
3//! ## Purpose of this crate
4//!
5//! This crate provides logic to compute and expose as inherent data aggregated information on block producers'
6//! and their delegators' participation in block production. This feature is implemented in an unusual way in
7//! that it publishes inherent data but leaves it to the specific Partner Chain's builders to implement the pallet
8//! that will consume this data. This is done because Partner Chains SDK can not anticipate how this data will
9//! have to be handled within every Partner Chain. The assumed use of the data provided is paying out of block
10//! production reward, however both ledger structure and reward calculation rules are inherently specific to
11//! their Partner Chain.
12//!
13//! ## Outline of operation
14//!
15//! 1. The inherent data provider calls runtime API to check whether it should release block participation inherent
16//!    data (all points below assume this check is positive) and gets the time limit up to which the blocks should be processed.
17//! 2. The inherent data provider retrieves data on block production up to the time limit using runtime API and Cardano
18//!    delegation data using observability data source. The IDP joins and aggregates this data together producing
19//!    block participation data.
20//! 3. The IDP puts the block participation data into the inherent data of the current block, under the inherent
21//!    identifier indicated by the runtime API. This inherent identifier belongs to the Partner Chain's custom
22//!    handler crate.
23//! 4. The IDP produces an additional "operational" inherent data to signal its own pallet that participation data
24//!    has been released.
25//! 5. The Partner Chain's custom handler pallet consumes the block participation inherent data and produces an
26//!    inherent that performs block rewards payouts or otherwise handles the data according to this particular
27//!    Partner Chain's rules.
28//! 6. The block participation pallet consumes the operational inherent data and cleans up block production data
29//!    up to the time limit.
30//!
31//! ## Usage
32//!
33//! To incorporate this feature into a Partner Chain, one must do the following:
34//! 1. Implement a pallet consuming inherent data of type [BlockProductionData]
35//! 2. Include the block participation pallet into their runtime and configure it. Consult the documentation of
36//!    `pallet_block_participation` for details.
37//! 3. Implement [BlockParticipationApi] for their runtime.
38//! 4. Include [inherent_data::BlockParticipationInherentDataProvider] in their node's inherent data
39//!    provider set for both proposal and verification of blocks.
40//!
41//! Configuring the pallet and implementing the runtime API requires there to be a source of block production data
42//! present in the runtime that can be used by the feature. The intended source is `pallet_block_production_log` but
43//! in principle any pallet offering a similar interfaces can be used. An example of runtime API implementation using
44//! the block participation log pallet looks like the following:
45//! ```rust,ignore
46//!	impl sp_block_participation::BlockParticipationApi<Block, BlockAuthor, Slot> for Runtime {
47//!		fn blocks_to_process(slot: &Slot) -> Vec<(Slot, BlockAuthor)> {
48//!			BlockParticipation::blocks_to_process(slot)
49//!		}
50//!		fn moment_to_timestamp_millis(moment: Slot) -> u64 {
51//!			let slot_duration_millis = <Self as sp_consensus_aura::runtime_decl_for_aura_api::AuraApi<Block, AuraId>>::slot_duration().as_millis();
52//!			*moment * slot_duration_millis
53//!		}
54//!		fn target_inherent_id() -> InherentIdentifier {
55//!			<Runtime as pallet_block_participation::Config>::TARGET_INHERENT_ID
56//!		}
57//!	}
58//! ```
59//!
60//! Note that the API uses a type parameter `Moment`, which in this case is `Slot`. This type must identify a moment
61//! in time in which a block can be produced, typically a timestamp or slot number. This type should be convertible
62//! into a timestamp in some way, eg. starting time in case of time ranges such as slots.
63//!
64#![cfg_attr(not(feature = "std"), no_std)]
65#![deny(missing_docs)]
66
67extern crate alloc;
68
69use alloc::vec::Vec;
70use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode};
71use scale_info::TypeInfo;
72use sidechain_domain::{DelegatorKey, MainchainKeyHash, McEpochNumber};
73use sp_inherents::{InherentIdentifier, IsFatalError};
74
75#[cfg(test)]
76mod tests;
77
78/// Inherent identifier used by the Block Participation pallet
79///
80/// This identifier is used for internal operation of the feature and is different from the target inherent ID
81/// provided through [BlockParticipationApi].
82pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"blokpart";
83
84/// Represents a block producer's delegator along with their number of shares in that block producer's pool.
85///
86/// Values of this type can only be interpreted in the context of their enclosing [BlockProducerParticipationData].
87#[derive(
88	Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialOrd, Ord,
89)]
90pub struct DelegatorBlockParticipationData<DelegatorId> {
91	/// Delegator Id
92	pub id: DelegatorId,
93	/// Number of this delegator's shares in the pool operated by the block producer of the enclosing [BlockProducerParticipationData].
94	pub share: u64,
95}
96
97/// Aggregated data on block production of one block producer in one aggregation period.
98///
99/// Values of this type can only be interpreted in the context of their enclosing [BlockProductionData].
100#[derive(
101	Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialOrd, Ord,
102)]
103pub struct BlockProducerParticipationData<BlockProducerId, DelegatorId> {
104	/// Block producer ID
105	pub block_producer: BlockProducerId,
106	/// Number of block produced in the aggregation period represented by the current [BlockProducerParticipationData]
107	pub block_count: u32,
108	/// Total sum of shares of delegators in `delegators` field
109	pub delegator_total_shares: u64,
110	/// List of delegators of `block_producer` along with their share in the block producer's stake pool
111	pub delegators: Vec<DelegatorBlockParticipationData<DelegatorId>>,
112}
113
114/// Aggregated data on block production, grouped by the block producer and aggregation period (main chain epoch).
115///
116/// When provided by the inherent data provider it should aggregate data since the previous processing
117#[derive(Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo)]
118pub struct BlockProductionData<BlockProducerId, DelegatorId> {
119	/// Aggregated data on block producers and their delegators.
120	///
121	/// There may be more than one entry for the same block producer in this collection if the aggregated
122	/// period spans multiple aggregation periods.
123	producer_participation: Vec<BlockProducerParticipationData<BlockProducerId, DelegatorId>>,
124}
125
126impl<BlockProducerId, DelegatorId> BlockProductionData<BlockProducerId, DelegatorId> {
127	/// Construct a new instance of [BlockProductionData], ensuring stable ordering of data.
128	pub fn new(
129		mut producer_participation: Vec<
130			BlockProducerParticipationData<BlockProducerId, DelegatorId>,
131		>,
132	) -> Self
133	where
134		BlockProducerId: Ord,
135		DelegatorId: Ord,
136	{
137		for breakdown in &mut producer_participation {
138			breakdown.delegators.sort()
139		}
140		producer_participation.sort();
141		Self { producer_participation }
142	}
143
144	/// Returns aggregated participation data per block producer
145	pub fn producer_participation(
146		&self,
147	) -> &[BlockProducerParticipationData<BlockProducerId, DelegatorId>] {
148		&self.producer_participation
149	}
150}
151
152/// Error type returned by the Block Participation pallet's inherent
153#[derive(Encode, PartialEq)]
154#[cfg_attr(not(feature = "std"), derive(Debug))]
155#[cfg_attr(
156	feature = "std",
157	derive(Decode, DecodeWithMemTracking, thiserror::Error, sp_runtime::RuntimeDebug)
158)]
159pub enum InherentError {
160	/// Indicates that inherent was not produced when expected
161	#[cfg_attr(feature = "std", error("Block participation inherent not produced when expected"))]
162	InherentRequired,
163	/// Indicates that inherent was produced when not expected
164	#[cfg_attr(feature = "std", error("Block participation inherent produced when not expected"))]
165	UnexpectedInherent,
166	/// Indicates that the inherent was produced with incorrect participation data
167	#[cfg_attr(feature = "std", error("Inherent data provided by the node is invalid"))]
168	InvalidInherentData,
169}
170
171impl IsFatalError for InherentError {
172	fn is_fatal_error(&self) -> bool {
173		true
174	}
175}
176
177sp_api::decl_runtime_apis! {
178	/// Runtime api exposing configuration and runtime bindings necessary for [inherent_data::BlockParticipationInherentDataProvider].
179	///
180	/// This API should typically be implemented by simply exposing relevant functions and data from the feature's pallet.
181	pub trait BlockParticipationApi<BlockProducerId: Decode, Moment: Decode + Encode> {
182		/// Returns block participation data that should be processed in the current block.
183		fn blocks_to_process(moment: &Moment) -> Vec<(Moment, BlockProducerId)>;
184		/// Returns the inherent ID under which block participation data should be provided.
185		fn target_inherent_id() -> InherentIdentifier;
186		/// Converts moment into a timestamp in UNIX milliseconds
187		fn moment_to_timestamp_millis(moment: Moment) -> u64;
188	}
189}
190
191/// Signifies that a type or some of its variants represents a Cardano stake pool operator
192pub trait AsCardanoSPO {
193	/// If [Self] represents a Cardano SPO, returns hash of this SPO's Cardano public key
194	fn as_cardano_spo(&self) -> Option<MainchainKeyHash>;
195}
196impl AsCardanoSPO for Option<MainchainKeyHash> {
197	fn as_cardano_spo(&self) -> Option<MainchainKeyHash> {
198		*self
199	}
200}
201
202/// Signifies that a type represents a Cardano delegator
203pub trait CardanoDelegator {
204	/// Converts a Cardano delegator key to [Self]
205	fn from_delegator_key(key: DelegatorKey) -> Self;
206}
207impl<T: From<DelegatorKey>> CardanoDelegator for T {
208	fn from_delegator_key(key: DelegatorKey) -> Self {
209		key.into()
210	}
211}
212
213/// Inherent data provider definitions and implementation for Block Producer feature
214#[cfg(feature = "std")]
215pub mod inherent_data {
216	use super::*;
217	use alloc::fmt::Debug;
218	use core::error::Error;
219	use core::ops::Deref;
220	use sidechain_domain::mainchain_epoch::*;
221	use sidechain_domain::*;
222	use sp_api::{ApiError, ApiExt, ProvideRuntimeApi};
223	use sp_inherents::{InherentData, InherentDataProvider};
224	use sp_runtime::traits::Block as BlockT;
225	use std::collections::HashMap;
226	use std::hash::Hash;
227
228	/// Cardano observability data source providing queries required by [BlockParticipationInherentDataProvider].
229	#[async_trait::async_trait]
230	pub trait BlockParticipationDataSource {
231		/// Retrieves stake pool delegation distribution for provided epoch and pools
232		async fn get_stake_pool_delegation_distribution_for_pools(
233			&self,
234			epoch: McEpochNumber,
235			pool_hashes: &[MainchainKeyHash],
236		) -> Result<StakeDistribution, Box<dyn std::error::Error + Send + Sync>>;
237	}
238
239	/// Error returned by [BlockParticipationInherentDataProvider] constructors
240	#[derive(thiserror::Error, Debug)]
241	pub enum InherentDataCreationError<BlockProducerId: Debug> {
242		/// Indicates that a runtime API failed
243		#[error("Runtime API call failed: {0}")]
244		ApiError(#[from] ApiError),
245		/// Indicates that a data source call returned an error
246		#[error("Data source call failed: {0}")]
247		DataSourceError(Box<dyn Error + Send + Sync>),
248		/// Indicates that Cardano stake delegation is missing for the epoch from which a block producer was selected
249		///
250		/// This error should never occur in normal operation of a node, unless the data source has been corrupted.
251		#[error("Missing epoch {0} data for {1:?}")]
252		DataMissing(McEpochNumber, BlockProducerId),
253		/// Indicates that the Cardano epoch covering a producer block could not be computed while respecting the
254		/// offset defined by [sidechain_domain::DATA_MC_EPOCH_OFFSET].
255		///
256		/// This error should never occur in normal operation of a node.
257		#[error("Offset of {1} can not be applied to main chain epoch {0}")]
258		McEpochBelowOffset(McEpochNumber, u32),
259	}
260
261	/// Inherent data provider for block participation data.
262	/// This IDP is active only if the `BlockParticipationApi::should_release_data` function returns `Some`.
263	/// This IDP provides two sets of inherent data:
264	/// - One is the block production data saved under the inherent ID indicated by the function
265	///   [BlockParticipationApi::target_inherent_id], which is intended for consumption by a chain-specific handler pallet.
266	/// - The other is the inherent data needed for internal operation of the feature which triggers clearing
267	///   of already handled data from the block production log pallet.
268	#[derive(Debug, Clone, PartialEq)]
269	pub enum BlockParticipationInherentDataProvider<BlockProducerId, DelegatorId, Moment> {
270		/// Active variant of the IDP that will provide inherent data stored in `block_production_data` at the
271		/// inherent ID stored in `target_inherent_id`.
272		Active {
273			/// Moment in time at which the block is being produced
274			moment: Moment,
275			/// Inherent ID under which inherent data will be provided
276			target_inherent_id: InherentIdentifier,
277			/// Inherent data containing aggregated block participation data
278			block_production_data: BlockProductionData<BlockProducerId, DelegatorId>,
279		},
280		/// Inactive variant of the IDP that will not provide any data and will not raise any errors
281		Inert,
282	}
283
284	impl<BlockProducer, Delegator, Moment>
285		BlockParticipationInherentDataProvider<BlockProducer, Delegator, Moment>
286	where
287		BlockProducer: AsCardanoSPO + Decode + Clone + Hash + Eq + Ord + Debug,
288		Delegator: CardanoDelegator + Ord + Debug,
289		Moment: Encode + Decode + Send + Sync,
290	{
291		/// Creates a new inherent data provider of block participation data.
292		///
293		/// The returned inherent data provider will be inactive if [ProvideRuntimeApi] is not present
294		/// in the runtime or if [BlockParticipationApi::should_release_data] returns [None].
295		pub async fn new<Block: BlockT, T>(
296			client: &T,
297			data_source: &(dyn BlockParticipationDataSource + Send + Sync),
298			parent_hash: <Block as BlockT>::Hash,
299			moment: Moment,
300			mc_epoch_config: &MainchainEpochConfig,
301		) -> Result<Self, InherentDataCreationError<BlockProducer>>
302		where
303			Moment: Decode + Encode,
304			T: ProvideRuntimeApi<Block> + Send + Sync,
305			T::Api: BlockParticipationApi<Block, BlockProducer, Moment>,
306		{
307			let api = client.runtime_api();
308
309			if !api
310				.has_api::<dyn BlockParticipationApi<Block, BlockProducer, Moment>>(parent_hash)?
311			{
312				return Ok(Self::Inert);
313			}
314
315			let blocks_to_process = api.blocks_to_process(parent_hash, &moment)?;
316
317			if blocks_to_process.is_empty() {
318				log::debug!("💤︎ Skipping computing block participation data this block...");
319				return Ok(Self::Inert);
320			};
321
322			let target_inherent_id = api.target_inherent_id(parent_hash)?;
323
324			let block_counts_by_epoch_and_producer = Self::count_blocks_by_epoch_and_producer(
325				blocks_to_process,
326				mc_epoch_config,
327				parent_hash,
328				api.deref(),
329			)?;
330
331			let mut production_summaries = vec![];
332			for (mc_epoch, producer_blocks) in block_counts_by_epoch_and_producer {
333				let stake_distribution =
334					Self::fetch_delegations(mc_epoch, producer_blocks.keys().cloned(), data_source)
335						.await?;
336				for (producer, block_count) in producer_blocks {
337					let breakdown = Self::production_breakdown_for(
338						mc_epoch,
339						producer,
340						block_count,
341						&stake_distribution,
342					)?;
343
344					production_summaries.push(breakdown);
345				}
346			}
347
348			Ok(Self::Active {
349				moment,
350				target_inherent_id,
351				block_production_data: BlockProductionData::new(production_summaries),
352			})
353		}
354
355		fn production_breakdown_for(
356			mc_epoch: McEpochNumber,
357			block_producer: BlockProducer,
358			block_count: u32,
359			distribution: &StakeDistribution,
360		) -> Result<
361			BlockProducerParticipationData<BlockProducer, Delegator>,
362			InherentDataCreationError<BlockProducer>,
363		> {
364			let (beneficiary_total_share, beneficiaries) = match block_producer.as_cardano_spo() {
365				None => (0, vec![]),
366				Some(cardano_producer) => {
367					let PoolDelegation { total_stake, delegators } =
368						distribution.0.get(&cardano_producer).ok_or_else(|| {
369							InherentDataCreationError::DataMissing(mc_epoch, block_producer.clone())
370						})?;
371					let beneficiaries = delegators
372						.iter()
373						.map(|(delegator_key, stake_amount)| DelegatorBlockParticipationData {
374							id: Delegator::from_delegator_key(delegator_key.clone()),
375							share: stake_amount.0.into(),
376						})
377						.collect();
378					(total_stake.0, beneficiaries)
379				},
380			};
381
382			Ok(BlockProducerParticipationData {
383				block_producer,
384				block_count,
385				delegator_total_shares: beneficiary_total_share,
386				delegators: beneficiaries,
387			})
388		}
389
390		async fn fetch_delegations(
391			mc_epoch: McEpochNumber,
392			producers: impl Iterator<Item = BlockProducer>,
393			data_source: &(dyn BlockParticipationDataSource + Send + Sync),
394		) -> Result<StakeDistribution, InherentDataCreationError<BlockProducer>> {
395			let pools: Vec<_> = producers.flat_map(|p| p.as_cardano_spo()).collect();
396			data_source
397				.get_stake_pool_delegation_distribution_for_pools(mc_epoch, &pools)
398				.await
399				.map_err(InherentDataCreationError::DataSourceError)
400		}
401
402		fn data_mc_epoch_for_timestamp(
403			timestamp: Timestamp,
404			mc_epoch_config: &MainchainEpochConfig,
405		) -> Result<McEpochNumber, InherentDataCreationError<BlockProducer>> {
406			let mc_epoch = mc_epoch_config
407				.timestamp_to_mainchain_epoch(timestamp)
408				.expect("Mainchain epoch for past timestamps exists");
409
410			offset_data_epoch(&mc_epoch)
411				.map_err(|offset| InherentDataCreationError::McEpochBelowOffset(mc_epoch, offset))
412		}
413
414		fn count_blocks_by_epoch_and_producer<Block: BlockT, Api>(
415			blocks: Vec<(Moment, BlockProducer)>,
416			mc_epoch_config: &MainchainEpochConfig,
417			parent_hash: Block::Hash,
418			api: &Api,
419		) -> Result<
420			HashMap<McEpochNumber, HashMap<BlockProducer, u32>>,
421			InherentDataCreationError<BlockProducer>,
422		>
423		where
424			Api: BlockParticipationApi<Block, BlockProducer, Moment>,
425		{
426			let mut epoch_producers: HashMap<McEpochNumber, HashMap<BlockProducer, u32>> =
427				HashMap::new();
428
429			for (moment, producer) in blocks {
430				let timestamp = Timestamp::from_unix_millis(
431					api.moment_to_timestamp_millis(parent_hash, moment)?,
432				);
433				let mc_epoch = Self::data_mc_epoch_for_timestamp(timestamp, mc_epoch_config)?;
434				let producer_block_count =
435					epoch_producers.entry(mc_epoch).or_default().entry(producer).or_default();
436
437				*producer_block_count += 1;
438			}
439
440			Ok(epoch_producers)
441		}
442	}
443
444	#[async_trait::async_trait]
445	impl<BlockProducerId, DelegatorId, Moment> InherentDataProvider
446		for BlockParticipationInherentDataProvider<BlockProducerId, DelegatorId, Moment>
447	where
448		DelegatorId: Encode + Send + Sync,
449		BlockProducerId: Encode + Send + Sync,
450		Moment: Encode + Send + Sync,
451	{
452		async fn provide_inherent_data(
453			&self,
454			inherent_data: &mut InherentData,
455		) -> Result<(), sp_inherents::Error> {
456			if let Self::Active { target_inherent_id, block_production_data, moment } = &self {
457				inherent_data.put_data(*target_inherent_id, block_production_data)?;
458				inherent_data.put_data(INHERENT_IDENTIFIER, &moment)?;
459			}
460			Ok(())
461		}
462
463		async fn try_handle_error(
464			&self,
465			identifier: &InherentIdentifier,
466			mut error: &[u8],
467		) -> Option<Result<(), sp_inherents::Error>> {
468			if *identifier == INHERENT_IDENTIFIER {
469				let err = match InherentError::decode(&mut error) {
470					Ok(error) => Box::from(error),
471					Err(decoding_err) => Box::from(format!(
472						"Undecodable block production inherent error: {decoding_err:?}"
473					)),
474				};
475
476				Some(Err(sp_inherents::Error::Application(err)))
477			} else {
478				None
479			}
480		}
481	}
482}