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 upper slot limit.
17//! 2. The inherent data provider retrieves data on block production up to the slot 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 slot 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 anu 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> for Runtime {
47//!		fn should_release_data(slot: Slot) -> Option<Slot> {
48//!			BlockParticipationPallet::should_release_data(slot)
49//!		}
50//!		fn blocks_produced_up_to_slot(slot: Slot) -> Vec<(Slot, BlockAuthor)> {
51//!			<Runtime as pallet_block_participation::Config>::blocks_produced_up_to_slot(slot).collect()
52//!		}
53//!		fn target_inherent_id() -> InherentIdentifier {
54//!			<Runtime as pallet_block_participation::Config>::TARGET_INHERENT_ID
55//!		}
56//!	}
57//! ```
58//!
59//!
60#![cfg_attr(not(feature = "std"), no_std)]
61#![deny(missing_docs)]
62
63extern crate alloc;
64
65use alloc::vec::Vec;
66use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode};
67use scale_info::TypeInfo;
68use sidechain_domain::{DelegatorKey, MainchainKeyHash, McEpochNumber};
69pub use sp_consensus_slots::{Slot, SlotDuration};
70use sp_inherents::{InherentIdentifier, IsFatalError};
71
72#[cfg(test)]
73mod tests;
74
75/// Inherent identifier used by the Block Participation pallet
76///
77/// This identifier is used for internal operation of the feature and is different from the target inherent ID
78/// provided through [BlockParticipationApi].
79pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"blokpart";
80
81/// Represents a block producer's delegator along with their number of shares in that block producer's pool.
82///
83/// Values of this type can only be interpreted in the context of their enclosing [BlockProducerParticipationData].
84#[derive(
85	Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialOrd, Ord,
86)]
87pub struct DelegatorBlockParticipationData<DelegatorId> {
88	/// Delegator Id
89	pub id: DelegatorId,
90	/// Number of this delegator's shares in the pool operated by the block producer of the enclosing [BlockProducerParticipationData].
91	pub share: u64,
92}
93
94/// Aggregated data on block production of one block producer in one aggregation period.
95///
96/// Values of this type can only be interpreted in the context of their enclosing [BlockProductionData].
97#[derive(
98	Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo, PartialOrd, Ord,
99)]
100pub struct BlockProducerParticipationData<BlockProducerId, DelegatorId> {
101	/// Block producer ID
102	pub block_producer: BlockProducerId,
103	/// Number of block produced in the aggregation period represented by the current [BlockProducerParticipationData]
104	pub block_count: u32,
105	/// Total sum of shares of delegators in `delegators` field
106	pub delegator_total_shares: u64,
107	/// List of delegators of `block_producer` along with their share in the block producer's stake pool
108	pub delegators: Vec<DelegatorBlockParticipationData<DelegatorId>>,
109}
110
111/// Aggregated data on block production, grouped by the block producer and aggregation period (main chain epoch).
112///
113/// When provided by the inherent data provider it should aggregate data since the previous `up_to_slot` to the current `up_to_slot`.
114#[derive(Clone, Debug, PartialEq, Eq, Decode, DecodeWithMemTracking, Encode, TypeInfo)]
115pub struct BlockProductionData<BlockProducerId, DelegatorId> {
116	/// Data upper slot boundary.
117	up_to_slot: Slot,
118	/// Aggregated data on block producers and their delegators.
119	///
120	/// There may be more than one entry for the same block producer in this collection if the aggregated
121	/// period spans multiple aggregation periods.
122	producer_participation: Vec<BlockProducerParticipationData<BlockProducerId, DelegatorId>>,
123}
124
125impl<BlockProducerId, DelegatorId> BlockProductionData<BlockProducerId, DelegatorId> {
126	/// Construct a new instance of [BlockProductionData], ensuring stable ordering of data.
127	pub fn new(
128		up_to_slot: Slot,
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 { up_to_slot, producer_participation }
142	}
143
144	/// Returns the upper slot boundary of the aggregation range of `self`
145	pub fn up_to_slot(&self) -> Slot {
146		self.up_to_slot
147	}
148
149	/// Returns aggregated participation data per block producer
150	pub fn producer_participation(
151		&self,
152	) -> &[BlockProducerParticipationData<BlockProducerId, DelegatorId>] {
153		&self.producer_participation
154	}
155}
156
157/// Error type returned by the Block Participation pallet's inherent
158#[derive(Encode, PartialEq)]
159#[cfg_attr(not(feature = "std"), derive(Debug))]
160#[cfg_attr(
161	feature = "std",
162	derive(Decode, DecodeWithMemTracking, thiserror::Error, sp_runtime::RuntimeDebug)
163)]
164pub enum InherentError {
165	/// Indicates that inherent was not produced when expected
166	#[cfg_attr(feature = "std", error("Block participation inherent not produced when expected"))]
167	InherentRequired,
168	/// Indicates that inherent was produced when not expected
169	#[cfg_attr(feature = "std", error("Block participation inherent produced when not expected"))]
170	UnexpectedInherent,
171	/// Indicates that the inherent was produced with incorrect slot boundary
172	#[cfg_attr(feature = "std", error("Block participation up_to_slot incorrect"))]
173	IncorrectSlotBoundary,
174	/// Indicates that the inherent was produced with incorrect participation data
175	#[cfg_attr(feature = "std", error("Inherent data provided by the node is invalid"))]
176	InvalidInherentData,
177}
178
179impl IsFatalError for InherentError {
180	fn is_fatal_error(&self) -> bool {
181		true
182	}
183}
184
185sp_api::decl_runtime_apis! {
186	/// Runtime api exposing configuration and runtime bindings necessary for [inherent_data::BlockParticipationInherentDataProvider].
187	///
188	/// This API should typically be implemented by simply exposing relevant functions and data from the feature's pallet.
189	pub trait BlockParticipationApi<BlockProducerId: Decode> {
190		/// Returns slot up to which block production data should be released or [None].
191		fn should_release_data(slot: Slot) -> Option<Slot>;
192		/// Returns block authors since last processing up to `slot`.
193		fn blocks_produced_up_to_slot(slot: Slot) -> Vec<(Slot, BlockProducerId)>;
194		/// Returns the inherent ID under which block participation data should be provided.
195		fn target_inherent_id() -> InherentIdentifier;
196	}
197}
198
199/// Signifies that a type or some of its variants represents a Cardano stake pool operator
200pub trait AsCardanoSPO {
201	/// If [Self] represents a Cardano SPO, returns hash of this SPO's Cardano public key
202	fn as_cardano_spo(&self) -> Option<MainchainKeyHash>;
203}
204impl AsCardanoSPO for Option<MainchainKeyHash> {
205	fn as_cardano_spo(&self) -> Option<MainchainKeyHash> {
206		*self
207	}
208}
209
210/// Signifies that a type represents a Cardano delegator
211pub trait CardanoDelegator {
212	/// Converts a Cardano delegator key to [Self]
213	fn from_delegator_key(key: DelegatorKey) -> Self;
214}
215impl<T: From<DelegatorKey>> CardanoDelegator for T {
216	fn from_delegator_key(key: DelegatorKey) -> Self {
217		key.into()
218	}
219}
220
221/// Inherent data provider definitions and implementation for Block Producer feature
222#[cfg(feature = "std")]
223pub mod inherent_data {
224	use super::*;
225	use alloc::fmt::Debug;
226	use core::error::Error;
227	use sidechain_domain::mainchain_epoch::*;
228	use sidechain_domain::*;
229	use sp_api::{ApiError, ApiExt, ProvideRuntimeApi};
230	use sp_inherents::{InherentData, InherentDataProvider};
231	use sp_runtime::traits::Block as BlockT;
232	use std::collections::HashMap;
233	use std::hash::Hash;
234
235	/// Cardano observability data source providing queries required by [BlockParticipationInherentDataProvider].
236	#[async_trait::async_trait]
237	pub trait BlockParticipationDataSource {
238		/// Retrieves stake pool delegation distribution for provided epoch and pools
239		async fn get_stake_pool_delegation_distribution_for_pools(
240			&self,
241			epoch: McEpochNumber,
242			pool_hashes: &[MainchainKeyHash],
243		) -> Result<StakeDistribution, Box<dyn std::error::Error + Send + Sync>>;
244	}
245
246	/// Error returned by [BlockParticipationInherentDataProvider] constructors
247	#[derive(thiserror::Error, Debug)]
248	pub enum InherentDataCreationError<BlockProducerId: Debug> {
249		/// Indicates that a runtime API failed
250		#[error("Runtime API call failed: {0}")]
251		ApiError(#[from] ApiError),
252		/// Indicates that a data source call returned an error
253		#[error("Data source call failed: {0}")]
254		DataSourceError(Box<dyn Error + Send + Sync>),
255		/// Indicates that Cardano stake delegation is missing for the epoch from which a block producer was selected
256		///
257		/// This error should never occur in normal operation of a node, unless the data source has been corrupted.
258		#[error("Missing epoch {0} data for {1:?}")]
259		DataMissing(McEpochNumber, BlockProducerId),
260		/// Indicates that the Cardano epoch covering a producer block could not be computed while respecting the
261		/// offset defined by [sidechain_domain::DATA_MC_EPOCH_OFFSET].
262		///
263		/// This error should never occur in normal operation of a node.
264		#[error("Offset of {1} can not be applied to main chain epoch {0}")]
265		McEpochBelowOffset(McEpochNumber, u32),
266	}
267
268	/// Inherent data provider for block participation data.
269	/// This IDP is active only if the `BlockParticipationApi::should_release_data` function returns `Some`.
270	/// This IDP provides two sets of inherent data:
271	/// - One is the block production data saved under the inherent ID indicated by the function
272	///   [BlockParticipationApi::target_inherent_id], which is intended for consumption by a chain-specific handler pallet.
273	/// - The other is the slot limit returned by [BlockParticipationApi::should_release_data]. This inherent data
274	///   is needed for internal operation of the feature and triggers clearing of already handled data
275	///   from the block production log pallet.
276	#[derive(Debug, Clone, PartialEq)]
277	pub enum BlockParticipationInherentDataProvider<BlockProducerId, DelegatorId> {
278		/// Active variant of the IDP that will provide inherent data stored in `block_production_data` at the
279		/// inherent ID stored in `target_inherent_id`.
280		Active {
281			/// Inherent ID under which inherent data will be provided
282			target_inherent_id: InherentIdentifier,
283			/// Inherent data containing aggregated block participation data
284			block_production_data: BlockProductionData<BlockProducerId, DelegatorId>,
285		},
286		/// Inactive variant of the IDP that will not provide any data and will not raise any errors
287		Inert,
288	}
289
290	impl<BlockProducer, Delegator> BlockParticipationInherentDataProvider<BlockProducer, Delegator>
291	where
292		BlockProducer: AsCardanoSPO + Decode + Clone + Hash + Eq + Ord + Debug,
293		Delegator: CardanoDelegator + Ord + Debug,
294	{
295		/// Creates a new inherent data provider of block participation data.
296		///
297		/// The returned inherent data provider will be inactive if [ProvideRuntimeApi] is not present
298		/// in the runtime or if [BlockParticipationApi::should_release_data] returns [None].
299		pub async fn new<Block: BlockT, T>(
300			client: &T,
301			data_source: &(dyn BlockParticipationDataSource + Send + Sync),
302			parent_hash: <Block as BlockT>::Hash,
303			current_slot: Slot,
304			mc_epoch_config: &MainchainEpochConfig,
305			slot_duration: SlotDuration,
306		) -> Result<Self, InherentDataCreationError<BlockProducer>>
307		where
308			T: ProvideRuntimeApi<Block> + Send + Sync,
309			T::Api: BlockParticipationApi<Block, BlockProducer>,
310		{
311			let api = client.runtime_api();
312
313			if !api.has_api::<dyn BlockParticipationApi<Block, BlockProducer>>(parent_hash)? {
314				return Ok(Self::Inert);
315			}
316
317			let Some(up_to_slot) = api.should_release_data(parent_hash, current_slot)? else {
318				log::debug!("💤︎ Skipping computing block participation data this block...");
319				return Ok(Self::Inert);
320			};
321			let blocks_produced_up_to_slot =
322				api.blocks_produced_up_to_slot(parent_hash, up_to_slot)?;
323			let target_inherent_id = api.target_inherent_id(parent_hash)?;
324
325			let block_counts_by_epoch_and_producer = Self::count_blocks_by_epoch_and_producer(
326				blocks_produced_up_to_slot,
327				mc_epoch_config,
328				slot_duration,
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				target_inherent_id,
350				block_production_data: BlockProductionData::new(up_to_slot, production_summaries),
351			})
352		}
353
354		fn production_breakdown_for(
355			mc_epoch: McEpochNumber,
356			block_producer: BlockProducer,
357			block_count: u32,
358			distribution: &StakeDistribution,
359		) -> Result<
360			BlockProducerParticipationData<BlockProducer, Delegator>,
361			InherentDataCreationError<BlockProducer>,
362		> {
363			let (beneficiary_total_share, beneficiaries) = match block_producer.as_cardano_spo() {
364				None => (0, vec![]),
365				Some(cardano_producer) => {
366					let PoolDelegation { total_stake, delegators } =
367						distribution.0.get(&cardano_producer).ok_or_else(|| {
368							InherentDataCreationError::DataMissing(mc_epoch, block_producer.clone())
369						})?;
370					let beneficiaries = delegators
371						.iter()
372						.map(|(delegator_key, stake_amount)| DelegatorBlockParticipationData {
373							id: Delegator::from_delegator_key(delegator_key.clone()),
374							share: stake_amount.0.into(),
375						})
376						.collect();
377					(total_stake.0, beneficiaries)
378				},
379			};
380
381			Ok(BlockProducerParticipationData {
382				block_producer,
383				block_count,
384				delegator_total_shares: beneficiary_total_share,
385				delegators: beneficiaries,
386			})
387		}
388
389		async fn fetch_delegations(
390			mc_epoch: McEpochNumber,
391			producers: impl Iterator<Item = BlockProducer>,
392			data_source: &(dyn BlockParticipationDataSource + Send + Sync),
393		) -> Result<StakeDistribution, InherentDataCreationError<BlockProducer>> {
394			let pools: Vec<_> = producers.flat_map(|p| p.as_cardano_spo()).collect();
395			data_source
396				.get_stake_pool_delegation_distribution_for_pools(mc_epoch, &pools)
397				.await
398				.map_err(InherentDataCreationError::DataSourceError)
399		}
400
401		fn data_mc_epoch_for_slot(
402			slot: Slot,
403			slot_duration: SlotDuration,
404			mc_epoch_config: &MainchainEpochConfig,
405		) -> Result<McEpochNumber, InherentDataCreationError<BlockProducer>> {
406			let timestamp = Timestamp::from_unix_millis(
407				slot.timestamp(slot_duration)
408					.expect("Timestamp for past slots can not overflow")
409					.as_millis(),
410			);
411			let mc_epoch = mc_epoch_config
412				.timestamp_to_mainchain_epoch(timestamp)
413				.expect("Mainchain epoch for past slots exists");
414
415			offset_data_epoch(&mc_epoch)
416				.map_err(|offset| InherentDataCreationError::McEpochBelowOffset(mc_epoch, offset))
417		}
418
419		fn count_blocks_by_epoch_and_producer(
420			slot_producers: Vec<(Slot, BlockProducer)>,
421			mc_epoch_config: &MainchainEpochConfig,
422			slot_duration: SlotDuration,
423		) -> Result<
424			HashMap<McEpochNumber, HashMap<BlockProducer, u32>>,
425			InherentDataCreationError<BlockProducer>,
426		> {
427			let mut epoch_producers: HashMap<McEpochNumber, HashMap<BlockProducer, u32>> =
428				HashMap::new();
429
430			for (slot, producer) in slot_producers {
431				let mc_epoch = Self::data_mc_epoch_for_slot(slot, slot_duration, mc_epoch_config)?;
432				let producer_block_count =
433					epoch_producers.entry(mc_epoch).or_default().entry(producer).or_default();
434
435				*producer_block_count += 1;
436			}
437
438			Ok(epoch_producers)
439		}
440	}
441
442	#[async_trait::async_trait]
443	impl<BlockProducerId, DelegatorId> InherentDataProvider
444		for BlockParticipationInherentDataProvider<BlockProducerId, DelegatorId>
445	where
446		DelegatorId: Encode + Send + Sync,
447		BlockProducerId: Encode + Send + Sync,
448	{
449		async fn provide_inherent_data(
450			&self,
451			inherent_data: &mut InherentData,
452		) -> Result<(), sp_inherents::Error> {
453			if let Self::Active { target_inherent_id, block_production_data } = &self {
454				inherent_data.put_data(*target_inherent_id, block_production_data)?;
455				inherent_data.put_data(INHERENT_IDENTIFIER, &block_production_data.up_to_slot)?;
456			}
457			Ok(())
458		}
459
460		async fn try_handle_error(
461			&self,
462			identifier: &InherentIdentifier,
463			mut error: &[u8],
464		) -> Option<Result<(), sp_inherents::Error>> {
465			if *identifier == INHERENT_IDENTIFIER {
466				let err = match InherentError::decode(&mut error) {
467					Ok(error) => Box::from(error),
468					Err(decoding_err) => Box::from(format!(
469						"Undecodable block production inherent error: {decoding_err:?}"
470					)),
471				};
472
473				Some(Err(sp_inherents::Error::Application(err)))
474			} else {
475				None
476			}
477		}
478	}
479}