From a41fb9a132faff5d8a2bf2054b8eef4251eab6ed Mon Sep 17 00:00:00 2001 From: Nikolaos Dymitriadis Date: Mon, 24 Feb 2025 17:26:24 +0100 Subject: [PATCH] wip Signed-off-by: Nikolaos Dymitriadis --- Cargo.lock | 3 + node/node/Cargo.toml | 1 + node/node/src/inherent_data.rs | 19 ++ node/node/src/service.rs | 1 + node/runtime/Cargo.toml | 4 + node/runtime/src/lib.rs | 39 ++++- .../pallets/block-rewards-payouts/src/lib.rs | 10 +- .../block-rewards-payouts/src/lib.rs | 162 +++++++++--------- toolkit/primitives/domain/src/lib.rs | 21 ++- 9 files changed, 165 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f44c5c003..d785f28cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6807,6 +6807,7 @@ dependencies = [ "sp-block-builder", "sp-block-production-log", "sp-block-rewards", + "sp-block-rewards-payouts", "sp-blockchain", "sp-consensus", "sp-consensus-aura", @@ -10024,6 +10025,7 @@ dependencies = [ "pallet-balances", "pallet-block-production-log", "pallet-block-rewards", + "pallet-block-rewards-payouts", "pallet-grandpa", "pallet-native-token-management", "pallet-partner-chains-session", @@ -10049,6 +10051,7 @@ dependencies = [ "sp-block-builder", "sp-block-production-log", "sp-block-rewards", + "sp-block-rewards-payouts", "sp-consensus-aura", "sp-consensus-grandpa", "sp-consensus-slots", diff --git a/node/node/Cargo.toml b/node/node/Cargo.toml index fb09116ca..17c252262 100644 --- a/node/node/Cargo.toml +++ b/node/node/Cargo.toml @@ -108,6 +108,7 @@ main-chain-follower-mock = { workspace = true, features = [ tokio = { workspace = true } cli-commands = { workspace = true } sp-stake-distribution = { workspace = true, features = ["std"] } +sp-block-rewards-payouts = { workspace = true, features = ["std"] } [build-dependencies] substrate-build-script-utils = { workspace = true } diff --git a/node/node/src/inherent_data.rs b/node/node/src/inherent_data.rs index b62e72e4b..ecc0d939e 100644 --- a/node/node/src/inherent_data.rs +++ b/node/node/src/inherent_data.rs @@ -8,6 +8,7 @@ use jsonrpsee::core::async_trait; use sc_consensus_aura::{find_pre_digest, SlotDuration}; use sc_service::Arc; use sidechain_domain::mainchain_epoch::MainchainEpochConfig; +use sidechain_domain::DelegatorKey; use sidechain_domain::{McBlockHash, ScEpochNumber}; use sidechain_mc_hash::McHashDataSource; use sidechain_mc_hash::McHashInherentDataProvider as McHashIDP; @@ -19,6 +20,8 @@ use sp_api::ProvideRuntimeApi; use sp_block_production_log::BlockAuthorInherentProvider; use sp_block_production_log::BlockProductionLogApi; use sp_block_rewards::BlockBeneficiaryInherentProvider; +use sp_block_rewards_payouts::inherent_data::BlockRewardsPayoutsInherentDataProvider; +use sp_block_rewards_payouts::BlockRewardsPayoutsApi; use sp_blockchain::HeaderBackend; use sp_consensus_aura::{ inherents::InherentDataProvider as AuraIDP, sr25519::AuthorityPair as AuraPair, Slot, @@ -32,6 +35,7 @@ use sp_native_token_management::{ use sp_partner_chains_consensus_aura::CurrentSlotProvider; use sp_runtime::traits::{Block as BlockT, Header, Zero}; use sp_session_validator_management::SessionValidatorManagementApi; +use sp_stake_distribution::StakeDistributionDataSource; use sp_timestamp::{InherentDataProvider as TimestampIDP, Timestamp}; use std::error::Error; use time_source::TimeSource; @@ -43,6 +47,7 @@ pub struct ProposalCIDP { mc_hash_data_source: Arc, authority_selection_data_source: Arc, native_token_data_source: Arc, + stake_distribution_data_source: Arc, } #[async_trait] @@ -58,6 +63,7 @@ where >, T::Api: NativeTokenManagementApi, T::Api: BlockProductionLogApi>, + T::Api: BlockRewardsPayoutsApi, { type InherentDataProviders = ( AuraIDP, @@ -67,6 +73,7 @@ where BlockAuthorInherentProvider, BlockBeneficiaryInherentProvider, NativeTokenIDP, + BlockRewardsPayoutsInherentDataProvider, ); async fn create_inherent_data_providers( @@ -80,6 +87,7 @@ where mc_hash_data_source, authority_selection_data_source, native_token_data_source, + stake_distribution_data_source, } = self; let CreateInherentDataConfig { mc_epoch_config, sc_slot_config, time_source } = config; @@ -117,6 +125,16 @@ where ) .await?; + let payouts = BlockRewardsPayoutsInherentDataProvider::new_cardano_stake_based( + client.as_ref(), + stake_distribution_data_source.as_ref(), + parent_hash, + *slot, + mc_epoch_config, + config.sc_slot_config.slot_duration, + ) + .await?; + Ok(( slot, timestamp, @@ -125,6 +143,7 @@ where block_producer_id_provider, block_beneficiary_provider, native_token, + payouts, )) } } diff --git a/node/node/src/service.rs b/node/node/src/service.rs index e521cdf2f..23d7a1d20 100644 --- a/node/node/src/service.rs +++ b/node/node/src/service.rs @@ -315,6 +315,7 @@ pub async fn new_full> for BlockAuthor { } } } +impl From for Option { + fn from(value: BlockAuthor) -> Self { + match value { + BlockAuthor::Incentivized(_, key) => Some(key.hash()), + _ => None, + } + } +} #[cfg(feature = "runtime-benchmarks")] impl From<[u8; 32]> for BlockAuthor { fn from(arr: [u8; 32]) -> Self { @@ -519,6 +530,18 @@ impl pallet_address_associations::Config for Runtime { } } +impl pallet_block_rewards_payouts::Config for Runtime { + type BeneficiaryId = DelegatorKey; + + fn should_pay_out(slot: sidechain_slots::Slot) -> Option { + if *slot % 3 == 0 { + Some(slot) + } else { + None + } + } +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub struct Runtime { @@ -536,6 +559,7 @@ construct_runtime!( AddressAssociations: pallet_address_associations, BlockRewards: pallet_block_rewards, BlockProductionLog: pallet_block_production_log, + BlockProductionPayouts: pallet_block_rewards_payouts, // pallet_grandpa reads pallet_session::pallet::CurrentIndex storage. // Only stub implementation of pallet_session should be wired. // Partner Chains session_manager ValidatorManagementSessionManager writes to pallet_session::pallet::CurrentIndex. @@ -926,6 +950,15 @@ impl_runtime_apis! { .clone() } } + + impl sp_block_rewards_payouts::BlockRewardsPayoutsApi for Runtime { + fn should_pay_out(slot: Slot) -> Option { + BlockProductionPayouts::should_pay_out(slot) + } + fn blocks_to_reward(slot: Slot) -> Vec<(Slot, BlockAuthor)> { + BlockProductionPayouts::blocks_to_reward(slot).collect() + } + } } #[cfg(test)] diff --git a/toolkit/pallets/block-rewards-payouts/src/lib.rs b/toolkit/pallets/block-rewards-payouts/src/lib.rs index ae0914928..60dcfee10 100644 --- a/toolkit/pallets/block-rewards-payouts/src/lib.rs +++ b/toolkit/pallets/block-rewards-payouts/src/lib.rs @@ -4,11 +4,6 @@ use frame_support::pallet_prelude::*; pub use pallet::*; use sp_block_rewards_payouts::*; -pub trait ShouldProcessPayout { - /// Should return slot up to which rewards should be paid out or None. - fn should_pay_out(slot: Slot) -> Option; -} - #[frame_support::pallet] pub mod pallet { use super::*; @@ -23,7 +18,8 @@ pub mod pallet { /// Type identifying reward receiver on the Partner Chain type BeneficiaryId: Member + Parameter + MaxEncodedLen; - type ShouldProcessPayout: ShouldProcessPayout; + /// Should return slot up to which rewards should be paid out or None. + fn should_pay_out(slot: Slot) -> Option; } #[pallet::inherent] @@ -97,7 +93,7 @@ pub mod pallet { impl Pallet { pub fn should_pay_out(slot: Slot) -> Option { - ::ShouldProcessPayout::should_pay_out(slot) + ::should_pay_out(slot) } pub fn blocks_to_reward( diff --git a/toolkit/primitives/block-rewards-payouts/src/lib.rs b/toolkit/primitives/block-rewards-payouts/src/lib.rs index de212a5ef..dd3ed39d3 100644 --- a/toolkit/primitives/block-rewards-payouts/src/lib.rs +++ b/toolkit/primitives/block-rewards-payouts/src/lib.rs @@ -5,29 +5,25 @@ extern crate alloc; use alloc::vec::Vec; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; +use sidechain_domain::McEpochNumber; pub use sp_consensus_slots::{Slot, SlotDuration}; use sp_inherents::{InherentIdentifier, IsFatalError}; pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"moneynow"; -#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo)] +#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo, PartialOrd, Ord)] pub struct BlockBeneficiaryInfo { id: BeneficiaryId, - share: u128, + share: u64, } -#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo)] -pub enum BlockProductionRundown { - Matched { - block_producer: BlockProducerId, - block_count: u32, - beneficiary_total_share: u128, - beneficiaries: Vec>, - }, - Unmatched { - block_producer: BlockProducerId, - block_count: u32, - }, +#[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo, PartialOrd, Ord)] +pub struct BlockProductionRundown { + block_producer: BlockProducerId, + mc_epoch: McEpochNumber, + block_count: u32, + beneficiary_total_share: u64, + beneficiaries: Vec>, } #[derive(Clone, Debug, PartialEq, Eq, Decode, Encode, TypeInfo)] @@ -54,7 +50,7 @@ impl IsFatalError for InherentError { sp_api::decl_runtime_apis! { pub trait BlockRewardsPayoutsApi { - /// Check if the payouts should be processed + /// Should return slot up to which rewards should be paid out or None. fn should_pay_out(slot: Slot) -> Option; fn blocks_to_reward(slot: Slot) -> Vec<(Slot, BlockProducerId)>; } @@ -63,7 +59,7 @@ sp_api::decl_runtime_apis! { #[cfg(feature = "std")] pub mod inherent_data { use super::*; - use alloc::collections::BTreeMap; + use alloc::fmt::Debug; use core::error::Error; use sidechain_domain::mainchain_epoch::*; use sidechain_domain::*; @@ -90,99 +86,99 @@ pub mod inherent_data { impl BlockRewardsPayoutsInherentDataProvider { - pub async fn new( + pub async fn new_cardano_stake_based( client: &T, - parent_hash: ::Hash, stake_distribution_data_source: &(dyn StakeDistributionDataSource + Send + Sync), - slot: Slot, + parent_hash: ::Hash, + current_slot: Slot, mc_epoch_config: &MainchainEpochConfig, slot_duration: SlotDuration, ) -> Result where T: ProvideRuntimeApi + Send + Sync, T::Api: BlockRewardsPayoutsApi, - BlockProducerId: Decode + Clone + Hash + Eq, + BlockProducerId: Decode + Clone + Hash + Eq + Ord + Debug, + BeneficiaryId: From + Ord + Debug, Option: From, - BlockProducerId: From, - BeneficiaryId: From, { let api = client.runtime_api(); - let Some(up_to_slot) = api.should_pay_out(parent_hash, slot)? else { - log::debug!("Skipping block rewards payouts this block..."); + let Some(up_to_slot) = api.should_pay_out(parent_hash, current_slot)? else { + log::debug!("💤︎ Skipping block rewards payouts this block..."); return Ok(Self { block_production_data: None }); }; + let blocks_to_reward = api.blocks_to_reward(parent_hash, up_to_slot)?; - let mut block_production_payout_info: BlockProductionPayoutInfo< - BlockProducerId, - BeneficiaryId, - > = BlockProductionPayoutInfo { up_to_slot, production_rundowns: vec![] }; - - let mut epochs = vec![]; - let mut pools: Vec = vec![]; - let mut epoch_producers: HashMap<(MainchainKeyHash, McEpochNumber), u32> = - HashMap::new(); - - for (slot, producer) in api.blocks_to_reward(parent_hash, slot)?.iter() { - let Some(producer) = >::from(producer.clone()) else { - continue; - }; - let mc_epoch = mc_epoch_config - .timestamp_to_mainchain_epoch(Timestamp::from_unix_millis( - slot.timestamp(slot_duration) - .expect("Timestamp for past slots can not overflow") - .as_millis(), - )) - .expect("Mainchain epoch for past slots exists"); - - epochs.push(mc_epoch); - pools.push(producer); - - *epoch_producers.entry((producer.clone(), mc_epoch)).or_default() += 1; - } + let mut block_production_payout_info = + BlockProductionPayoutInfo { up_to_slot, production_rundowns: vec![] }; + + let epoch_map = + count_by_epoch_and_producer(blocks_to_reward, mc_epoch_config, slot_duration); - let mut epoch_stakes = BTreeMap::new(); - for epoch in epochs { + for (mc_epoch, producers) in epoch_map.into_iter() { + let pools: Vec<_> = + producers.keys().cloned().flat_map(Into::>::into).collect(); let distribution = stake_distribution_data_source - .get_stake_pool_delegation_distribution_for_pools(epoch, &pools) + .get_stake_pool_delegation_distribution_for_pools(mc_epoch, &pools) .await .map_err(InherentDataCreationError::DataSourceError)?; - epoch_stakes.insert(epoch, distribution); + for (producer, block_count) in producers.into_iter() { + if let Some(cardano_producer) = producer.clone().into() { + let PoolDelegation { total_stake, delegators } = + distribution.0.get(&cardano_producer).expect(""); + let beneficiaries = delegators + .iter() + .map(|(delegator_id, stake_amount)| BlockBeneficiaryInfo { + id: BeneficiaryId::from(delegator_id.clone()), + share: stake_amount.0.into(), + }) + .collect(); + let rundown = BlockProductionRundown { + block_producer: producer, + mc_epoch, + block_count, + beneficiary_total_share: total_stake.0, + beneficiaries, + }; + block_production_payout_info.production_rundowns.push(rundown); + } + } } + block_production_payout_info.production_rundowns.sort(); - for ((block_producer, mc_epoch), block_count) in epoch_producers.into_iter() { - let rundown = match epoch_stakes - .get(&mc_epoch) - .expect("Epochs in epoch_producers and epoch_stakes are the same") - .0 - .get(&block_producer) - { - None => BlockProductionRundown::Unmatched { - block_producer: block_producer.into(), - block_count, - }, - Some(PoolDelegation { total_stake, delegators }) => { - BlockProductionRundown::Matched { - block_producer: block_producer.into(), - block_count, - beneficiary_total_share: total_stake.0.into(), - beneficiaries: delegators - .into_iter() - .map(|(delegator_id, stake_amount)| BlockBeneficiaryInfo { - id: BeneficiaryId::from(delegator_id.clone()), - share: stake_amount.0.into(), - }) - .collect(), - } - }, - }; - - block_production_payout_info.production_rundowns.push(rundown) + for rundown in &mut block_production_payout_info.production_rundowns { + rundown.beneficiaries.sort() } + println!("payout time! {block_production_payout_info:?}"); + Ok(Self { block_production_data: Some(block_production_payout_info) }) } } + fn count_by_epoch_and_producer( + slot_producers: Vec<(Slot, BlockProducerId)>, + mc_epoch_config: &MainchainEpochConfig, + slot_duration: SlotDuration, + ) -> HashMap> { + let mut epoch_producers: HashMap> = + HashMap::new(); + + for (slot, producer) in slot_producers { + let mc_epoch = mc_epoch_config + .timestamp_to_mainchain_epoch(Timestamp::from_unix_millis( + slot.timestamp(slot_duration) + .expect("Timestamp for past slots can not overflow") + .as_millis(), + )) + .expect("Mainchain epoch for past slots exists"); + let mc_epoch = McEpochNumber(mc_epoch.0 - 2); + + *epoch_producers.entry(mc_epoch).or_default().entry(producer).or_default() += 1; + } + + epoch_producers + } + #[async_trait::async_trait] impl InherentDataProvider for BlockRewardsPayoutsInherentDataProvider diff --git a/toolkit/primitives/domain/src/lib.rs b/toolkit/primitives/domain/src/lib.rs index 1ab463d65..8dc844a47 100644 --- a/toolkit/primitives/domain/src/lib.rs +++ b/toolkit/primitives/domain/src/lib.rs @@ -252,7 +252,9 @@ impl FromStr for AssetId { const STAKE_POOL_PUBLIC_KEY_LEN: usize = 32; -#[derive(Clone, PartialEq, Eq, Encode, Decode, ToDatum, TypeInfo, MaxEncodedLen, Hash)] +#[derive( + Clone, PartialEq, Eq, Encode, Decode, ToDatum, TypeInfo, MaxEncodedLen, Hash, Ord, PartialOrd, +)] #[cfg_attr(feature = "std", byte_string(to_hex_string))] #[byte_string(debug, hex_serialize, hex_deserialize, decode_hex)] pub struct StakePoolPublicKey(pub [u8; STAKE_POOL_PUBLIC_KEY_LEN]); @@ -860,12 +862,27 @@ mod tests { } } -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, TypeInfo, MaxEncodedLen, Encode, Decode)] pub enum DelegatorKey { StakeKeyHash([u8; 28]), ScriptKeyHash { hash_raw: [u8; 28], script_hash: [u8; 28] }, } +impl alloc::fmt::Debug for DelegatorKey { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let s = match self { + Self::ScriptKeyHash { hash_raw, script_hash } => alloc::format!( + "ScriptKeyHash{{ hash_raw: {}, script_hash: {} }}", + hex::encode(hash_raw), + hex::encode(script_hash) + ), + Self::StakeKeyHash(hash) => alloc::format!("StakeKeyHash({})", hex::encode(hash)), + }; + + f.write_str(&s) + } +} + #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct DelegatorStakeAmount(pub u64);