From 0ee59b0e56d4cc8eeab8b80121fe99dd79ddcc72 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 19 Feb 2025 10:37:57 +0200 Subject: [PATCH 01/10] Cleanup in `ah-client` --- substrate/frame/staking/ah-client/src/lib.rs | 25 ++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index 88aee9ee3e9da..32ac3ac12c0a6 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -82,20 +82,13 @@ pub mod pallet { /// The balance type of this pallet. pub type BalanceOf = ::CurrencyBalance; - // `Exposure>` will be removed. This type alias exists only to - // suppress clippy warnings. - type ElectedValidatorSet = Vec<( - ::AccountId, - Exposure<::AccountId, BalanceOf>, - )>; - #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); - // TODO: should contain some initial state, otherwise starting from genesis won't work #[pallet::storage] - pub type ValidatorSet = StorageValue<_, Option>, ValueQuery>; + pub type ValidatorSet = + StorageValue<_, Option::AccountId>>, ValueQuery>; /// Keeps track of the session points for each block author in the current session. #[pallet::storage] @@ -137,7 +130,7 @@ pub mod pallet { // #[pallet::weight(T::WeightInfo::new_validators())] // TODO pub fn new_validator_set( origin: OriginFor, - new_validator_set: ElectedValidatorSet, + new_validator_set: Vec, ) -> DispatchResult { // Ignore requests not coming from the AssetHub or root. Self::ensure_root_or_para(origin, ::AssetHubId::get().into())?; @@ -149,18 +142,20 @@ pub mod pallet { } } - impl historical::SessionManager>> - for Pallet - { - fn new_session(_: sp_staking::SessionIndex) -> Option> { + impl historical::SessionManager for Pallet { + fn new_session( + _: sp_staking::SessionIndex, + ) -> Option::AccountId, ())>> { // If there is a new validator set - return it. Otherwise return `None`. ValidatorSet::::take() + .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) } fn new_session_genesis( _: SessionIndex, - ) -> Option>)>> { + ) -> Option::AccountId, ())>> { ValidatorSet::::take() + .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) } fn start_session(start_index: SessionIndex) { From 2bbd8cc28d8d62ea92607af7ee9e9c1ae9c28df1 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 20 Feb 2025 14:23:42 +0200 Subject: [PATCH 02/10] Fix a log message --- substrate/frame/staking/rc-client/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/staking/rc-client/src/lib.rs b/substrate/frame/staking/rc-client/src/lib.rs index dc6c0b7e5c6fc..bd23a6ba7671f 100644 --- a/substrate/frame/staking/rc-client/src/lib.rs +++ b/substrate/frame/staking/rc-client/src/lib.rs @@ -125,7 +125,7 @@ pub mod pallet { ]); if let Err(err) = send_xcm::(Location::new(1, Here), message) { - log::error!(target: LOG_TARGET, "Sending `NewValidators` to relay chain failed: {:?}", err); + log::error!(target: LOG_TARGET, "Sending `NewValidatorSet` to relay chain failed: {:?}", err); } } } From d495e4bcc949cc0d5acfb2e4deefa68984e1a40c Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 13 Feb 2025 14:28:35 +0200 Subject: [PATCH 03/10] Add `pallet-staking-ah-client` to Westend it and plug in in `pallet-session-historical` instead of staking pallet --- Cargo.lock | 1 + polkadot/runtime/westend/Cargo.toml | 4 +++ polkadot/runtime/westend/src/lib.rs | 31 ++++++++++++++++---- substrate/frame/staking/ah-client/src/lib.rs | 17 +++-------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6fefcfdfe186..596e2bf2bb625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31528,6 +31528,7 @@ dependencies = [ "pallet-session-benchmarking 28.0.0", "pallet-society 28.0.0", "pallet-staking 28.0.0", + "pallet-staking-ah-client", "pallet-staking-runtime-api 14.0.0", "pallet-state-trie-migration 29.0.0", "pallet-sudo 28.0.0", diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index 3317484419a9a..7925b9ad52946 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -86,6 +86,7 @@ pallet-scheduler = { workspace = true } pallet-session = { workspace = true } pallet-society = { workspace = true } pallet-staking = { workspace = true } +pallet-staking-ah-client = { workspace = true } pallet-staking-runtime-api = { workspace = true } pallet-state-trie-migration = { workspace = true } pallet-sudo = { workspace = true } @@ -186,6 +187,7 @@ std = [ "pallet-session-benchmarking?/std", "pallet-session/std", "pallet-society/std", + "pallet-staking-ah-client/std", "pallet-staking-runtime-api/std", "pallet-staking/std", "pallet-state-trie-migration/std", @@ -273,6 +275,7 @@ runtime-benchmarks = [ "pallet-session-benchmarking/runtime-benchmarks", "pallet-society/runtime-benchmarks", "pallet-staking/runtime-benchmarks", + "pallet-staking-ah-client/runtime-benchmarks", "pallet-state-trie-migration/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", @@ -334,6 +337,7 @@ try-runtime = [ "pallet-session/try-runtime", "pallet-society/try-runtime", "pallet-staking/try-runtime", + "pallet-staking-ah-client/try-runtime", "pallet-state-trie-migration/try-runtime", "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 8ee9e073f162d..035130904ea4c 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -134,7 +134,7 @@ pub use sp_runtime::BuildStorage; use westend_runtime_constants::{ currency::*, fee::*, - system_parachain::{coretime::TIMESLICE_PERIOD, BROKER_ID}, + system_parachain::{coretime::TIMESLICE_PERIOD, ASSET_HUB_ID, BROKER_ID}, time::*, }; @@ -504,7 +504,7 @@ impl pallet_timestamp::Config for Runtime { impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = Staking; + type EventHandler = AssetHubStakingClient; } parameter_types! { @@ -529,16 +529,25 @@ impl pallet_session::Config for Runtime { type ValidatorIdOf = pallet_staking::StashOf; type ShouldEndSession = Babe; type NextSessionRotation = Babe; - type SessionManager = pallet_session::historical::NoteHistoricalRoot; + type SessionManager = + pallet_session::historical::NoteHistoricalRoot; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type DisablingStrategy = pallet_session::disabling::UpToLimitWithReEnablingDisablingStrategy; type WeightInfo = weights::pallet_session::WeightInfo; } +// Dummy implementation which returns `Some(())` +pub struct FullIdentificationOf; +impl sp_runtime::traits::Convert> for FullIdentificationOf { + fn convert(_: AccountId) -> Option<()> { + Some(Default::default()) + } +} + impl pallet_session::historical::Config for Runtime { - type FullIdentification = pallet_staking::Exposure; - type FullIdentificationOf = pallet_staking::ExposureOf; + type FullIdentification = (); + type FullIdentificationOf = FullIdentificationOf; } pub struct MaybeSignedPhase; @@ -774,6 +783,13 @@ impl pallet_staking::Config for Runtime { type MaxDisabledValidators = ConstU32<100>; } +impl pallet_staking_ah_client::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type CurrencyBalance = Balance; + type AssetHubId = AssetHubId; + type SendXcm = crate::xcm_config::XcmRouter; +} + impl pallet_fast_unstake::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -848,7 +864,7 @@ impl pallet_treasury::Config for Runtime { impl pallet_offences::Config for Runtime { type RuntimeEvent = RuntimeEvent; type IdentificationTuple = pallet_session::historical::IdentificationTuple; - type OnOffenceHandler = Staking; + type OnOffenceHandler = AssetHubStakingClient; } impl pallet_authority_discovery::Config for Runtime { @@ -1353,6 +1369,7 @@ impl parachains_scheduler::Config for Runtime { parameter_types! { pub const BrokerId: u32 = BROKER_ID; + pub const AssetHubId: u32 = ASSET_HUB_ID; // TODO: replace with ASSET_HUB_NEXT_ID pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); pub MaxXcmTransactWeight: Weight = Weight::from_parts(200_000_000, 20_000); } @@ -1785,6 +1802,8 @@ mod runtime { pub type AssignedSlots = assigned_slots; #[runtime::pallet_index(66)] pub type Coretime = coretime; + #[runtime::pallet_index(67)] + pub type AssetHubStakingClient = pallet_staking_ah_client; // Migrations pallet #[runtime::pallet_index(98)] diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index 32ac3ac12c0a6..c70853f221fa6 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -31,10 +31,10 @@ extern crate alloc; use alloc::vec::Vec; use frame_support::pallet_prelude::*; +pub use pallet::*; use pallet_staking_rc_client::Offence; use sp_core::crypto::AccountId32; -use sp_runtime::traits::Convert; -use sp_staking::{offence::OffenceDetails, Exposure, SessionIndex}; +use sp_staking::{offence::OffenceDetails, SessionIndex}; use xcm::prelude::*; const LOG_TARGET: &str = "runtime::staking::ah-client"; @@ -71,7 +71,6 @@ pub mod pallet { use core::result; use frame_system::pallet_prelude::*; use pallet_session::historical; - use pallet_staking::ExposureOf; use polkadot_primitives::Id as ParaId; use polkadot_runtime_parachains::origin::{ensure_parachain, Origin}; use sp_runtime::Perbill; @@ -227,21 +226,13 @@ pub mod pallet { } } - impl + impl>> OnOffenceHandler, Weight> for Pallet where T: pallet_session::Config::AccountId>, - T: pallet_session::historical::Config< - FullIdentification = Exposure<::AccountId, BalanceOf>, - FullIdentificationOf = ExposureOf, - >, - T::SessionHandler: pallet_session::SessionHandler<::AccountId>, + T: pallet_session::historical::Config, T::SessionManager: pallet_session::SessionManager<::AccountId>, - T::ValidatorIdOf: Convert< - ::AccountId, - Option<::AccountId>, - >, T::AccountId: Into, { fn on_offence( From c73ab05324fb10f8ff72596a4c52e93c89a75015 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 21 Feb 2025 13:52:29 +0200 Subject: [PATCH 04/10] Report rewards in parachains pallet via a trait instead of calling staking pallet directly --- Cargo.lock | 1 - polkadot/runtime/parachains/Cargo.toml | 4 ---- polkadot/runtime/parachains/src/lib.rs | 5 ++++ .../runtime/parachains/src/reward_points.rs | 24 ++++++++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 596e2bf2bb625..2639fe7c84845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18669,7 +18669,6 @@ dependencies = [ "pallet-message-queue 31.0.0", "pallet-mmr 27.0.0", "pallet-session 28.0.0", - "pallet-staking 28.0.0", "pallet-timestamp 27.0.0", "parity-scale-codec", "polkadot-core-primitives 7.0.0", diff --git a/polkadot/runtime/parachains/Cargo.toml b/polkadot/runtime/parachains/Cargo.toml index 6c87f7773c235..42290bc76b551 100644 --- a/polkadot/runtime/parachains/Cargo.toml +++ b/polkadot/runtime/parachains/Cargo.toml @@ -45,7 +45,6 @@ pallet-broker = { workspace = true } pallet-message-queue = { workspace = true } pallet-mmr = { workspace = true, optional = true } pallet-session = { workspace = true } -pallet-staking = { workspace = true } pallet-timestamp = { workspace = true } polkadot-primitives = { workspace = true } @@ -93,7 +92,6 @@ std = [ "pallet-message-queue/std", "pallet-mmr?/std", "pallet-session/std", - "pallet-staking/std", "pallet-timestamp/std", "polkadot-core-primitives/std", "polkadot-parachain-primitives/std", @@ -127,7 +125,6 @@ runtime-benchmarks = [ "pallet-broker/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-mmr/runtime-benchmarks", - "pallet-staking/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", "polkadot-primitives/runtime-benchmarks", @@ -151,7 +148,6 @@ try-runtime = [ "pallet-message-queue/try-runtime", "pallet-mmr/try-runtime", "pallet-session/try-runtime", - "pallet-staking/try-runtime", "pallet-timestamp/try-runtime", "sp-runtime/try-runtime", ] diff --git a/polkadot/runtime/parachains/src/lib.rs b/polkadot/runtime/parachains/src/lib.rs index b1ff5419470e1..de5737118fa27 100644 --- a/polkadot/runtime/parachains/src/lib.rs +++ b/polkadot/runtime/parachains/src/lib.rs @@ -78,6 +78,11 @@ pub trait FeeTracker { fn decrease_fee_factor(id: Self::Id) -> FixedU128; } +/// Trait for reporting rewards for processing parachains blocks +pub trait RewardsReporter { + fn reward_by_ids(validators_points: impl IntoIterator); +} + /// Schedule a para to be initialized at the start of the next session with the given genesis data. pub fn schedule_para_initialize( id: ParaId, diff --git a/polkadot/runtime/parachains/src/reward_points.rs b/polkadot/runtime/parachains/src/reward_points.rs index 69ef2db756c21..f427f2fc19d3b 100644 --- a/polkadot/runtime/parachains/src/reward_points.rs +++ b/polkadot/runtime/parachains/src/reward_points.rs @@ -21,7 +21,7 @@ //! which doesn't currently mention availability bitfields. As such, we don't reward them //! for the time being, although we will build schemes to do so in the future. -use crate::{session_info, shared}; +use crate::{session_info, shared, RewardsReporter}; use alloc::collections::btree_set::BTreeSet; use frame_support::traits::{Defensive, ValidatorSet}; use polkadot_primitives::{SessionIndex, ValidatorIndex}; @@ -32,12 +32,16 @@ pub const BACKING_POINTS: u32 = 20; pub const DISPUTE_STATEMENT_POINTS: u32 = 20; /// Rewards validators for participating in parachains with era points in pallet-staking. -pub struct RewardValidatorsWithEraPoints(core::marker::PhantomData); +pub struct RewardValidatorsWithEraPoints( + core::marker::PhantomData, + core::marker::PhantomData, +); -impl RewardValidatorsWithEraPoints +impl RewardValidatorsWithEraPoints where - C: pallet_staking::Config + session_info::Config, + C: session_info::Config, C::ValidatorSet: ValidatorSet, + R: RewardsReporter, { /// Reward validators in session with points, but only if they are in the active set. fn reward_only_active( @@ -61,14 +65,15 @@ where .filter(|v| active_set.contains(v)) .map(|v| (v, points)); - >::reward_by_ids(rewards); + R::reward_by_ids(rewards); } } -impl crate::inclusion::RewardValidators for RewardValidatorsWithEraPoints +impl crate::inclusion::RewardValidators for RewardValidatorsWithEraPoints where - C: pallet_staking::Config + shared::Config + session_info::Config, + C: shared::Config + session_info::Config, C::ValidatorSet: ValidatorSet, + R: RewardsReporter, { fn reward_backing(indices: impl IntoIterator) { let session_index = shared::CurrentSessionIndex::::get(); @@ -78,10 +83,11 @@ where fn reward_bitfields(_validators: impl IntoIterator) {} } -impl crate::disputes::RewardValidators for RewardValidatorsWithEraPoints +impl crate::disputes::RewardValidators for RewardValidatorsWithEraPoints where - C: pallet_staking::Config + session_info::Config, + C: session_info::Config, C::ValidatorSet: ValidatorSet, + R: RewardsReporter, { fn reward_dispute_statement( session: SessionIndex, From ab6e63ab2a5259900a2855245d2aa0b6b2e2018b Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 21 Feb 2025 15:03:51 +0200 Subject: [PATCH 05/10] Hook ah_client as a rewards handler --- polkadot/runtime/westend/src/lib.rs | 13 +++++++++++-- substrate/frame/staking/ah-client/src/lib.rs | 6 ++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 035130904ea4c..ac41547de5fdb 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1266,10 +1266,18 @@ impl parachains_session_info::Config for Runtime { type ValidatorSet = Historical; } +pub struct RewardsHandler; +impl polkadot_runtime_parachains::RewardsReporter for RewardsHandler { + fn reward_by_ids(validators_points: impl IntoIterator) { + >::handle_parachain_rewards(validators_points); + } +} + impl parachains_inclusion::Config for Runtime { type RuntimeEvent = RuntimeEvent; type DisputesHandler = ParasDisputes; - type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; + type RewardValidators = + parachains_reward_points::RewardValidatorsWithEraPoints; type MessageQueue = MessageQueue; type WeightInfo = weights::polkadot_runtime_parachains_inclusion::WeightInfo; } @@ -1442,7 +1450,8 @@ impl assigned_slots::Config for Runtime { impl parachains_disputes::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type RewardValidators = parachains_reward_points::RewardValidatorsWithEraPoints; + type RewardValidators = + parachains_reward_points::RewardValidatorsWithEraPoints; type SlashingHandler = parachains_slashing::SlashValidatorsForDisputes; type WeightInfo = weights::polkadot_runtime_parachains_disputes::WeightInfo; } diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index c70853f221fa6..d62aaf798c1cf 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -296,6 +296,12 @@ pub mod pallet { } Ok(()) } + + pub fn handle_parachain_rewards( + validators_points: impl IntoIterator, + ) { + todo!() + } } fn mk_asset_hub_call(call: StakingCalls) -> Instruction<()> { From d0b3daa5bd134f3c11fadac5dbb0be3508c7a72e Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Mon, 24 Feb 2025 13:37:45 +0200 Subject: [PATCH 06/10] Handle block authorship rewards --- substrate/frame/staking/ah-client/src/lib.rs | 37 +++++++++++++++++--- substrate/frame/staking/rc-client/src/lib.rs | 19 ++++++++-- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index d62aaf798c1cf..9278cb1303af2 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -62,6 +62,9 @@ enum StakingCalls { /// Report one or more offences. #[codec(index = 2)] NewRelayChainOffences(SessionIndex, Vec), + /// Report rewards from parachain blocks processing. + #[codec(index = 3)] + ParachainSessionPoints(Vec<(AccountId32, u32)>), } #[frame_support::pallet(dev_mode)] @@ -124,7 +127,10 @@ pub mod pallet { } #[pallet::call] - impl Pallet { + impl Pallet + where + T::AccountId: Into, + { #[pallet::call_index(0)] // #[pallet::weight(T::WeightInfo::new_validators())] // TODO pub fn new_validator_set( @@ -279,7 +285,10 @@ pub mod pallet { } } - impl Pallet { + impl Pallet + where + T::AccountId: Into, + { /// Ensure the origin is one of Root or the `para` itself. fn ensure_root_or_para( origin: ::RuntimeOrigin, @@ -299,8 +308,28 @@ pub mod pallet { pub fn handle_parachain_rewards( validators_points: impl IntoIterator, - ) { - todo!() + ) -> Weight { + let parachain_points = validators_points + .into_iter() + .map(|(id, points)| (id.into(), points)) + .collect::>(); + + let message = Xcm(vec![ + Instruction::UnpaidExecution { + weight_limit: WeightLimit::Unlimited, + check_origin: None, + }, + mk_asset_hub_call(StakingCalls::ParachainSessionPoints(parachain_points)), + ]); + if let Err(err) = send_xcm::( + Location::new(0, [Junction::Parachain(T::AssetHubId::get())]), + message, + ) { + log::error!(target: LOG_TARGET, "Sending `ParachainSessionPoints` to AssetHub failed: {:?}", + err); + } + + Weight::zero() } } diff --git a/substrate/frame/staking/rc-client/src/lib.rs b/substrate/frame/staking/rc-client/src/lib.rs index bd23a6ba7671f..1acf1f7e1c4c5 100644 --- a/substrate/frame/staking/rc-client/src/lib.rs +++ b/substrate/frame/staking/rc-client/src/lib.rs @@ -46,6 +46,8 @@ pub trait StakingApi { fn on_relay_chain_session_end(end_index: SessionIndex, block_authors: Vec<(AccountId32, u32)>); /// Report one or more offences on the relay chain. fn on_new_offences(offences: Vec); + /// Report validator rewards. + fn reward_by_ids(validators_points: Vec<(AccountId32, u32)>); } /// `pallet-staking-ah-client` pallet index on Relay chain. Used to construct remote calls. @@ -134,7 +136,7 @@ pub mod pallet { impl Pallet { /// Called to indicate the start of a new session on the relay chain. #[pallet::call_index(0)] - // #[pallet::weight(T::WeightInfo::end_session())] // TODO + // #[pallet::weight(T::WeightInfo::relay_chain_session_start())] // TODO pub fn relay_chain_session_start( origin: OriginFor, start_index: SessionIndex, @@ -147,7 +149,7 @@ pub mod pallet { /// Called to indicate the end of a session on the relay chain. Accepts the session id and /// the block authors with their corresponding session points for the finished session. #[pallet::call_index(1)] - // #[pallet::weight(T::WeightInfo::end_session())] // TODO + // #[pallet::weight(T::WeightInfo::relay_chain_session_end())] // TODO pub fn relay_chain_session_end( origin: OriginFor, end_index: SessionIndex, @@ -160,7 +162,7 @@ pub mod pallet { /// Called to report one or more new offenses on the relay chain. #[pallet::call_index(2)] - // #[pallet::weight(T::WeightInfo::end_session())] // TODO + // #[pallet::weight(T::WeightInfo::new_relay_chain_offence())] // TODO pub fn new_relay_chain_offence( origin: OriginFor, offences: Vec, @@ -169,6 +171,17 @@ pub mod pallet { T::StakingApi::on_new_offences(offences); Ok(()) } + + #[pallet::call_index(3)] + // #[pallet::weight(T::WeightInfo::end_session())] // TODO + pub fn parachain_session_points( + origin: OriginFor, + session_points: Vec<(AccountId32, u32)>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin_or_root(origin)?; + T::StakingApi::reward_by_ids(session_points); + Ok(()) + } } fn mk_relay_chain_call(call: SessionCalls) -> Instruction<()> { From 9bcb38926ce68c5fb4f8b9521af72771a398f35d Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Wed, 26 Feb 2025 14:24:43 +0200 Subject: [PATCH 07/10] wip - revisit this again --- Cargo.lock | 1 + substrate/frame/staking/Cargo.toml | 6 + substrate/frame/staking/rc-client/src/lib.rs | 11 +- substrate/frame/staking/src/lib.rs | 28 +- substrate/frame/staking/src/pallet/impls.rs | 444 ++++++++++++------- substrate/frame/staking/src/pallet/mod.rs | 4 +- 6 files changed, 296 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2639fe7c84845..38d7b2452f355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15516,6 +15516,7 @@ dependencies = [ "pallet-bags-list 27.0.0", "pallet-balances 28.0.0", "pallet-session 28.0.0", + "pallet-staking-rc-client", "pallet-staking-reward-curve", "pallet-timestamp 27.0.0", "parity-scale-codec", diff --git a/substrate/frame/staking/Cargo.toml b/substrate/frame/staking/Cargo.toml index ee69c29af1672..03f43ba12fd4e 100644 --- a/substrate/frame/staking/Cargo.toml +++ b/substrate/frame/staking/Cargo.toml @@ -24,9 +24,12 @@ frame-support = { workspace = true } frame-system = { workspace = true } log = { workspace = true } pallet-authorship = { workspace = true } +# Should be removed after `pallet_session::migrations::v1::MigrateDisabledValidators` and +# `MigrateV14ToV15` are executed pallet-session = { features = [ "historical", ], workspace = true } +pallet-staking-rc-client = { workspace = true } rand = { features = ["alloc"], workspace = true } rand_chacha = { workspace = true } scale-info = { features = ["derive", "serde"], workspace = true } @@ -66,6 +69,7 @@ std = [ "pallet-bags-list/std", "pallet-balances/std", "pallet-session/std", + "pallet-staking-rc-client/std", "pallet-timestamp/std", "rand/std", "rand_chacha/std", @@ -87,6 +91,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-staking-rc-client/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "sp-staking/runtime-benchmarks", @@ -99,6 +104,7 @@ try-runtime = [ "pallet-bags-list/try-runtime", "pallet-balances/try-runtime", "pallet-session/try-runtime", + "pallet-staking-rc-client/try-runtime", "pallet-timestamp/try-runtime", "sp-runtime/try-runtime", ] diff --git a/substrate/frame/staking/rc-client/src/lib.rs b/substrate/frame/staking/rc-client/src/lib.rs index 1acf1f7e1c4c5..7fadd15bcc12f 100644 --- a/substrate/frame/staking/rc-client/src/lib.rs +++ b/substrate/frame/staking/rc-client/src/lib.rs @@ -45,7 +45,7 @@ pub trait StakingApi { /// corresponding session points are reported. fn on_relay_chain_session_end(end_index: SessionIndex, block_authors: Vec<(AccountId32, u32)>); /// Report one or more offences on the relay chain. - fn on_new_offences(offences: Vec); + fn on_new_offences(slash_session: SessionIndex, offences: Vec) -> Weight; /// Report validator rewards. fn reward_by_ids(validators_points: Vec<(AccountId32, u32)>); } @@ -70,9 +70,9 @@ enum SessionCalls { // An offence on the relay chain. Based on [`sp_staking::offence::OffenceDetails`]. #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo)] pub struct Offence { - offender: AccountId32, - reporters: Vec, - slash_fraction: Perbill, + pub offender: AccountId32, + pub reporters: Vec, + pub slash_fraction: Perbill, } impl Offence { @@ -165,10 +165,11 @@ pub mod pallet { // #[pallet::weight(T::WeightInfo::new_relay_chain_offence())] // TODO pub fn new_relay_chain_offence( origin: OriginFor, + slash_session: SessionIndex, offences: Vec, ) -> DispatchResult { T::AdminOrigin::ensure_origin_or_root(origin)?; - T::StakingApi::on_new_offences(offences); + T::StakingApi::on_new_offences(slash_session, offences); Ok(()) } diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 9105a3e7ec13a..ca0ef3058edb8 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -902,33 +902,7 @@ pub trait SessionInterface { fn prune_historical_up_to(up_to: SessionIndex); } -impl SessionInterface<::AccountId> for T -where - T: pallet_session::Config::AccountId>, - T: pallet_session::historical::Config, - T::SessionHandler: pallet_session::SessionHandler<::AccountId>, - T::SessionManager: pallet_session::SessionManager<::AccountId>, - T::ValidatorIdOf: Convert< - ::AccountId, - Option<::AccountId>, - >, -{ - fn report_offence( - validator: ::AccountId, - severity: OffenceSeverity, - ) { - >::report_offence(validator, severity) - } - - fn validators() -> Vec<::AccountId> { - >::validators() - } - - fn prune_historical_up_to(up_to: SessionIndex) { - >::prune_up_to(up_to); - } -} - +// TODO: Why we need this? impl SessionInterface for () { fn report_offence(_validator: AccountId, _severity: OffenceSeverity) { () diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 0406c1dbbb2d8..19f87c9f3e5d9 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -32,14 +32,15 @@ use frame_support::{ weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; -use pallet_session::historical; +use pallet_staking_rc_client::{Offence, StakingApi}; +use sp_core::crypto::AccountId32; use sp_runtime::{ traits::{Bounded, CheckedAdd, Convert, SaturatedConversion, Saturating, StaticLookup, Zero}, ArithmeticError, DispatchResult, Perbill, Percent, }; use sp_staking::{ currency_to_vote::CurrencyToVote, - offence::{OffenceDetails, OffenceSeverity, OnOffenceHandler}, + offence::{OffenceSeverity, OnOffenceHandler}, EraIndex, OnStakingUpdate, Page, SessionIndex, Stake, StakingAccount::{self, Controller, Stash}, StakingInterface, @@ -1664,158 +1665,23 @@ impl ElectionDataProvider for Pallet { } } -/// In this implementation `new_session(session)` must be called before `end_session(session-1)` -/// i.e. the new session must be planned before the ending of the previous session. -/// -/// Once the first new_session is planned, all session must start and then end in order, though -/// some session can lag in between the newest session planned and the latest session started. -impl pallet_session::SessionManager for Pallet { - // └── Self::new_session(new_index, false) - // └── Self::try_plan_new_era(session_index, is_genesis) - // └── T::GenesisElectionProvider::elect() OR ElectableStashes::::take() - // └── Self::collect_exposures() - // └── Self::store_stakers_info() - // └── Self::plan_new_era() - // └── CurrentEra increment - // └── ErasStartSessionIndex update - // └── Self::clear_era_information() - fn new_session(new_index: SessionIndex) -> Option> { - log!(trace, "planning new session {}", new_index); - CurrentPlannedSession::::put(new_index); - Self::new_session(new_index, false).map(|v| v.into_inner()) - } - fn new_session_genesis(new_index: SessionIndex) -> Option> { - log!(trace, "planning new session {} at genesis", new_index); - CurrentPlannedSession::::put(new_index); - Self::new_session(new_index, true).map(|v| v.into_inner()) - } - // start_session(start_session: SessionIndex) - // └── Check if this is the start of next active era - // └── Self::start_era(start_session) - // └── Update active era index - // └── Set active era start timestamp - // └── Update BondedEras - // └── Self::apply_unapplied_slashes() - // └── Get slashes for era from UnappliedSlashes - // └── Apply each slash - // └── Clear slashes metadata - // └── Process disabled validators - // └── Get all disabled validators - // └── Call T::SessionInterface::disable_validator() for each - fn start_session(start_index: SessionIndex) { +impl StakingApi for Pallet +where + T::AccountId: From, +{ + fn on_relay_chain_session_start(start_index: SessionIndex) { log!(trace, "starting session {}", start_index); Self::start_session(start_index) } - fn end_session(end_index: SessionIndex) { + + fn on_relay_chain_session_end(end_index: SessionIndex, block_authors: Vec<(AccountId32, u32)>) { log!(trace, "ending session {}", end_index); + // TODO: process rewards for `block_authors` Self::end_session(end_index) } -} - -impl historical::SessionManager>> - for Pallet -{ - fn new_session( - new_index: SessionIndex, - ) -> Option>)>> { - >::new_session(new_index).map(|validators| { - let current_era = CurrentEra::::get() - // Must be some as a new era has been created. - .unwrap_or(0); - - validators - .into_iter() - .map(|v| { - let exposure = Self::eras_stakers(current_era, &v); - (v, exposure) - }) - .collect() - }) - } - fn new_session_genesis( - new_index: SessionIndex, - ) -> Option>)>> { - >::new_session_genesis(new_index).map( - |validators| { - let current_era = CurrentEra::::get() - // Must be some as a new era has been created. - .unwrap_or(0); - - validators - .into_iter() - .map(|v| { - let exposure = Self::eras_stakers(current_era, &v); - (v, exposure) - }) - .collect() - }, - ) - } - fn start_session(start_index: SessionIndex) { - >::start_session(start_index) - } - fn end_session(end_index: SessionIndex) { - >::end_session(end_index) - } -} - -impl historical::SessionManager for Pallet { - fn new_session(new_index: SessionIndex) -> Option> { - >::new_session(new_index) - .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) - } - fn new_session_genesis(new_index: SessionIndex) -> Option> { - >::new_session_genesis(new_index) - .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) - } - fn start_session(start_index: SessionIndex) { - >::start_session(start_index) - } - fn end_session(end_index: SessionIndex) { - >::end_session(end_index) - } -} - -/// Add reward points to block authors: -/// * 20 points to the block producer for producing a (non-uncle) block, -impl pallet_authorship::EventHandler> for Pallet -where - T: Config + pallet_authorship::Config + pallet_session::Config, -{ - fn note_author(author: T::AccountId) { - Self::reward_by_ids(vec![(author, 20)]) - } -} -/// This is intended to be used with `FilterHistoricalOffences`. -impl - OnOffenceHandler, Weight> - for Pallet -where - T: pallet_session::Config::AccountId>, - T: pallet_session::historical::Config, - T::SessionHandler: pallet_session::SessionHandler<::AccountId>, - T::SessionManager: pallet_session::SessionManager<::AccountId>, - T::ValidatorIdOf: Convert< - ::AccountId, - Option<::AccountId>, - >, -{ - /// When an offence is reported, it is split into pages and put in the offence queue. - /// As offence queue is processed, computed slashes are queued to be applied after the - /// `SlashDeferDuration`. - fn on_offence( - offenders: &[OffenceDetails>], - slash_fractions: &[Perbill], - slash_session: SessionIndex, - ) -> Weight { - log!( - debug, - "🦹 on_offence: offenders={:?}, slash_fractions={:?}, slash_session={}", - offenders, - slash_fractions, - slash_session, - ); + fn on_new_offences(slash_session: SessionIndex, offences: Vec) -> Weight { + log!(debug, "🦹 on_new_offences: {:?}", offences); // todo(ank4n): Needs to be properly benched. let mut consumed_weight = Weight::zero(); @@ -1826,7 +1692,7 @@ where // Find the era to which offence belongs. add_db_reads_writes(1, 0); let Some(active_era) = ActiveEra::::get() else { - log!(warn, "🦹 on_offence: no active era; ignoring offence"); + log!(warn, "🦹 on_new_offences: no active era; ignoring offence"); return consumed_weight }; @@ -1860,8 +1726,9 @@ where add_db_reads_writes(1, 0); let invulnerables = Invulnerables::::get(); - for (details, slash_fraction) in offenders.iter().zip(slash_fractions) { - let (validator, _) = &details.offender; + for o in offences { + let slash_fraction = o.slash_fraction; + let validator: ::AccountId = o.offender.into(); // Skip if the validator is invulnerable. if invulnerables.contains(&validator) { log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator); @@ -1869,7 +1736,8 @@ where } add_db_reads_writes(1, 0); - let Some(exposure_overview) = >::get(&offence_era, validator) + let Some(exposure_overview) = + >::get(&offence_era, validator.clone()) else { // defensive: this implies offence is for a discarded era, and should already be // filtered out. @@ -1884,7 +1752,7 @@ where Self::deposit_event(Event::::OffenceReported { validator: validator.clone(), - fraction: *slash_fraction, + fraction: slash_fraction, offence_era, }); @@ -1894,24 +1762,25 @@ where add_db_reads_writes(2, 2); T::SessionInterface::report_offence( validator.clone(), - OffenceSeverity(*slash_fraction), + OffenceSeverity(slash_fraction), ); } add_db_reads_writes(1, 0); - let prior_slash_fraction = ValidatorSlashInEra::::get(offence_era, validator) - .map_or(Zero::zero(), |(f, _)| f); + let prior_slash_fraction = + ValidatorSlashInEra::::get(offence_era, validator.clone()) + .map_or(Zero::zero(), |(f, _)| f); add_db_reads_writes(1, 0); - if let Some(existing) = OffenceQueue::::get(offence_era, validator) { + if let Some(existing) = OffenceQueue::::get(offence_era, validator.clone()) { if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() { add_db_reads_writes(0, 2); OffenceQueue::::insert( offence_era, - validator, + validator.clone(), OffenceRecord { - reporter: details.reporters.first().cloned(), + reporter: o.reporters.first().cloned(), reported_era: active_era.index, - slash_fraction: *slash_fraction, + slash_fraction, ..existing }, ); @@ -1919,7 +1788,7 @@ where // update the slash fraction in the `ValidatorSlashInEra` storage. ValidatorSlashInEra::::insert( offence_era, - validator, + validator.clone(), (slash_fraction, exposure_overview.own), ); @@ -1943,20 +1812,20 @@ where add_db_reads_writes(0, 3); ValidatorSlashInEra::::insert( offence_era, - validator, + validator.clone(), (slash_fraction, exposure_overview.own), ); OffenceQueue::::insert( offence_era, - validator, + validator.clone(), OffenceRecord { - reporter: details.reporters.first().cloned(), + reporter: o.reporters.first().cloned(), reported_era: active_era.index, // there are cases of validator with no exposure, hence 0 page, so we // saturate to avoid underflow. exposure_page: exposure_overview.page_count.saturating_sub(1), - slash_fraction: *slash_fraction, + slash_fraction, prior_slash_fraction, }, ); @@ -1995,8 +1864,255 @@ where consumed_weight } + + fn reward_by_ids(validators_points: Vec<(AccountId32, u32)>) {} } +// TODO: is `new_session` still needed? If no - who sets `CurrentPlannedSession` +/// In this implementation `new_session(session)` must be called before `end_session(session-1)` +/// i.e. the new session must be planned before the ending of the previous session. +/// +/// Once the first new_session is planned, all session must start and then end in order, though +/// some session can lag in between the newest session planned and the latest session started. +// impl pallet_session::SessionManager for Pallet { +// // └── Self::new_session(new_index, false) +// // └── Self::try_plan_new_era(session_index, is_genesis) +// // └── T::GenesisElectionProvider::elect() OR ElectableStashes::::take() +// // └── Self::collect_exposures() +// // └── Self::store_stakers_info() +// // └── Self::plan_new_era() +// // └── CurrentEra increment +// // └── ErasStartSessionIndex update +// // └── Self::clear_era_information() +// fn new_session(new_index: SessionIndex) -> Option> { +// log!(trace, "planning new session {}", new_index); +// CurrentPlannedSession::::put(new_index); +// Self::new_session(new_index, false).map(|v| v.into_inner()) +// } +// fn new_session_genesis(new_index: SessionIndex) -> Option> { +// log!(trace, "planning new session {} at genesis", new_index); +// CurrentPlannedSession::::put(new_index); +// Self::new_session(new_index, true).map(|v| v.into_inner()) +// } +// } + +/// Add reward points to block authors: +/// * 20 points to the block producer for producing a (non-uncle) block, +impl pallet_authorship::EventHandler> for Pallet +where + T: Config + pallet_authorship::Config, +{ + fn note_author(author: T::AccountId) { + Self::reward_by_ids(vec![(author, 20)]) + } +} + +/// This is intended to be used with `FilterHistoricalOffences`. +// impl +// OnOffenceHandler, Weight> +// for Pallet +// where +// T::ValidatorIdOf: Convert< +// ::AccountId, +// Option<::AccountId>, +// >, +// { +// /// When an offence is reported, it is split into pages and put in the offence queue. +// /// As offence queue is processed, computed slashes are queued to be applied after the +// /// `SlashDeferDuration`. +// fn on_offence( +// offenders: &[OffenceDetails>], +// slash_fractions: &[Perbill], +// slash_session: SessionIndex, +// ) -> Weight { +// log!( +// debug, +// "🦹 on_offence: offenders={:?}, slash_fractions={:?}, slash_session={}", +// offenders, +// slash_fractions, +// slash_session, +// ); + +// // todo(ank4n): Needs to be properly benched. +// let mut consumed_weight = Weight::zero(); +// let mut add_db_reads_writes = |reads, writes| { +// consumed_weight += T::DbWeight::get().reads_writes(reads, writes); +// }; + +// // Find the era to which offence belongs. +// add_db_reads_writes(1, 0); +// let Some(active_era) = ActiveEra::::get() else { +// log!(warn, "🦹 on_offence: no active era; ignoring offence"); +// return consumed_weight +// }; + +// add_db_reads_writes(1, 0); +// let active_era_start_session = +// ErasStartSessionIndex::::get(active_era.index).unwrap_or(0); + +// // Fast path for active-era report - most likely. +// // `slash_session` cannot be in a future active era. It must be in `active_era` or before. +// let offence_era = if slash_session >= active_era_start_session { +// active_era.index +// } else { +// add_db_reads_writes(1, 0); +// match BondedEras::::get() +// .iter() +// // Reverse because it's more likely to find reports from recent eras. +// .rev() +// .find(|&(_, sesh)| sesh <= &slash_session) +// .map(|(era, _)| *era) +// { +// Some(era) => era, +// None => { +// // defensive: this implies offence is for a discarded era, and should already be +// // filtered out. +// log!(warn, "🦹 on_offence: no era found for slash_session; ignoring offence"); +// return Weight::default() +// }, +// } +// }; + +// add_db_reads_writes(1, 0); +// let invulnerables = Invulnerables::::get(); + +// for (details, slash_fraction) in offenders.iter().zip(slash_fractions) { +// let (validator, _) = &details.offender; +// // Skip if the validator is invulnerable. +// if invulnerables.contains(&validator) { +// log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator); +// continue +// } + +// add_db_reads_writes(1, 0); +// let Some(exposure_overview) = >::get(&offence_era, validator) +// else { +// // defensive: this implies offence is for a discarded era, and should already be +// // filtered out. +// log!( +// warn, +// "🦹 on_offence: no exposure found for {:?} in era {}; ignoring offence", +// validator, +// offence_era +// ); +// continue; +// }; + +// Self::deposit_event(Event::::OffenceReported { +// validator: validator.clone(), +// fraction: *slash_fraction, +// offence_era, +// }); + +// if offence_era == active_era.index { +// // offence is in the current active era. Report it to session to maybe disable the +// // validator. +// add_db_reads_writes(2, 2); +// T::SessionInterface::report_offence( +// validator.clone(), +// OffenceSeverity(*slash_fraction), +// ); +// } +// add_db_reads_writes(1, 0); +// let prior_slash_fraction = ValidatorSlashInEra::::get(offence_era, validator) +// .map_or(Zero::zero(), |(f, _)| f); + +// add_db_reads_writes(1, 0); +// if let Some(existing) = OffenceQueue::::get(offence_era, validator) { +// if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() { +// add_db_reads_writes(0, 2); +// OffenceQueue::::insert( +// offence_era, +// validator, +// OffenceRecord { +// reporter: details.reporters.first().cloned(), +// reported_era: active_era.index, +// slash_fraction: *slash_fraction, +// ..existing +// }, +// ); + +// // update the slash fraction in the `ValidatorSlashInEra` storage. +// ValidatorSlashInEra::::insert( +// offence_era, +// validator, +// (slash_fraction, exposure_overview.own), +// ); + +// log!( +// debug, +// "🦹 updated slash for {:?}: {:?} (prior: {:?})", +// validator, +// slash_fraction, +// prior_slash_fraction, +// ); +// } else { +// log!( +// debug, +// "🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})", +// validator, +// slash_fraction, +// prior_slash_fraction, +// ); +// } +// } else if slash_fraction.deconstruct() > prior_slash_fraction.deconstruct() { +// add_db_reads_writes(0, 3); +// ValidatorSlashInEra::::insert( +// offence_era, +// validator, +// (slash_fraction, exposure_overview.own), +// ); + +// OffenceQueue::::insert( +// offence_era, +// validator, +// OffenceRecord { +// reporter: details.reporters.first().cloned(), +// reported_era: active_era.index, +// // there are cases of validator with no exposure, hence 0 page, so we +// // saturate to avoid underflow. +// exposure_page: exposure_overview.page_count.saturating_sub(1), +// slash_fraction: *slash_fraction, +// prior_slash_fraction, +// }, +// ); + +// OffenceQueueEras::::mutate(|q| { +// if let Some(eras) = q { +// log!(debug, "🦹 inserting offence era {} into existing queue", offence_era); +// eras.binary_search(&offence_era) +// .err() +// .map(|idx| eras.try_insert(idx, offence_era).defensive()); +// } else { +// let mut eras = BoundedVec::default(); +// log!(debug, "🦹 inserting offence era {} into empty queue", offence_era); +// let _ = eras.try_push(offence_era).defensive(); +// *q = Some(eras); +// } +// }); + +// log!( +// debug, +// "🦹 queued slash for {:?}: {:?} (prior: {:?})", +// validator, +// slash_fraction, +// prior_slash_fraction, +// ); +// } else { +// log!( +// debug, +// "🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})", +// validator, +// slash_fraction, +// prior_slash_fraction, +// ); +// } +// } + +// consumed_weight +// } +// } + impl ScoreProvider for Pallet { type Score = VoteWeight; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 4184ac3bdde81..438b8c0e344fb 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -43,7 +43,7 @@ use rand_chacha::{ use sp_core::{sr25519::Pair as SrPair, Pair}; use sp_runtime::{ traits::{SaturatedConversion, StaticLookup, Zero}, - ArithmeticError, Perbill, Percent, Saturating, + AccountId32, ArithmeticError, Perbill, Percent, Saturating, }; use sp_staking::{ @@ -94,7 +94,7 @@ pub mod pallet { } #[pallet::config(with_default)] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config { /// The old trait for staking balance. Deprecated and only used for migrating old ledgers. #[pallet::no_default] type OldCurrency: InspectLockableCurrency< From 87c9cea3259948fab7dfa58a7843e49a6003474a Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 27 Feb 2025 10:22:31 +0200 Subject: [PATCH 08/10] Reward block authors, remove `clone`-s, remove dead code --- substrate/frame/staking/src/pallet/impls.rs | 238 ++------------------ 1 file changed, 15 insertions(+), 223 deletions(-) diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 19f87c9f3e5d9..2c0e18d11b801 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -1676,7 +1676,14 @@ where fn on_relay_chain_session_end(end_index: SessionIndex, block_authors: Vec<(AccountId32, u32)>) { log!(trace, "ending session {}", end_index); - // TODO: process rewards for `block_authors` + + // reward block authors + for (author, block_count) in block_authors { + let author: T::AccountId = author.into(); + let block_count: u32 = block_count; + Self::reward_by_ids(vec![(author, 20 * block_count)]) + } + Self::end_session(end_index) } @@ -1737,7 +1744,7 @@ where add_db_reads_writes(1, 0); let Some(exposure_overview) = - >::get(&offence_era, validator.clone()) + >::get(&offence_era, &validator) else { // defensive: this implies offence is for a discarded era, and should already be // filtered out. @@ -1767,16 +1774,16 @@ where } add_db_reads_writes(1, 0); let prior_slash_fraction = - ValidatorSlashInEra::::get(offence_era, validator.clone()) + ValidatorSlashInEra::::get(offence_era, &validator) .map_or(Zero::zero(), |(f, _)| f); add_db_reads_writes(1, 0); - if let Some(existing) = OffenceQueue::::get(offence_era, validator.clone()) { + if let Some(existing) = OffenceQueue::::get(offence_era, &validator) { if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() { add_db_reads_writes(0, 2); OffenceQueue::::insert( offence_era, - validator.clone(), + &validator, OffenceRecord { reporter: o.reporters.first().cloned(), reported_era: active_era.index, @@ -1788,7 +1795,7 @@ where // update the slash fraction in the `ValidatorSlashInEra` storage. ValidatorSlashInEra::::insert( offence_era, - validator.clone(), + &validator, (slash_fraction, exposure_overview.own), ); @@ -1812,13 +1819,13 @@ where add_db_reads_writes(0, 3); ValidatorSlashInEra::::insert( offence_era, - validator.clone(), + &validator, (slash_fraction, exposure_overview.own), ); OffenceQueue::::insert( offence_era, - validator.clone(), + &validator, OffenceRecord { reporter: o.reporters.first().cloned(), reported_era: active_era.index, @@ -1896,222 +1903,7 @@ where // } // } -/// Add reward points to block authors: -/// * 20 points to the block producer for producing a (non-uncle) block, -impl pallet_authorship::EventHandler> for Pallet -where - T: Config + pallet_authorship::Config, -{ - fn note_author(author: T::AccountId) { - Self::reward_by_ids(vec![(author, 20)]) - } -} -/// This is intended to be used with `FilterHistoricalOffences`. -// impl -// OnOffenceHandler, Weight> -// for Pallet -// where -// T::ValidatorIdOf: Convert< -// ::AccountId, -// Option<::AccountId>, -// >, -// { -// /// When an offence is reported, it is split into pages and put in the offence queue. -// /// As offence queue is processed, computed slashes are queued to be applied after the -// /// `SlashDeferDuration`. -// fn on_offence( -// offenders: &[OffenceDetails>], -// slash_fractions: &[Perbill], -// slash_session: SessionIndex, -// ) -> Weight { -// log!( -// debug, -// "🦹 on_offence: offenders={:?}, slash_fractions={:?}, slash_session={}", -// offenders, -// slash_fractions, -// slash_session, -// ); - -// // todo(ank4n): Needs to be properly benched. -// let mut consumed_weight = Weight::zero(); -// let mut add_db_reads_writes = |reads, writes| { -// consumed_weight += T::DbWeight::get().reads_writes(reads, writes); -// }; - -// // Find the era to which offence belongs. -// add_db_reads_writes(1, 0); -// let Some(active_era) = ActiveEra::::get() else { -// log!(warn, "🦹 on_offence: no active era; ignoring offence"); -// return consumed_weight -// }; - -// add_db_reads_writes(1, 0); -// let active_era_start_session = -// ErasStartSessionIndex::::get(active_era.index).unwrap_or(0); - -// // Fast path for active-era report - most likely. -// // `slash_session` cannot be in a future active era. It must be in `active_era` or before. -// let offence_era = if slash_session >= active_era_start_session { -// active_era.index -// } else { -// add_db_reads_writes(1, 0); -// match BondedEras::::get() -// .iter() -// // Reverse because it's more likely to find reports from recent eras. -// .rev() -// .find(|&(_, sesh)| sesh <= &slash_session) -// .map(|(era, _)| *era) -// { -// Some(era) => era, -// None => { -// // defensive: this implies offence is for a discarded era, and should already be -// // filtered out. -// log!(warn, "🦹 on_offence: no era found for slash_session; ignoring offence"); -// return Weight::default() -// }, -// } -// }; - -// add_db_reads_writes(1, 0); -// let invulnerables = Invulnerables::::get(); - -// for (details, slash_fraction) in offenders.iter().zip(slash_fractions) { -// let (validator, _) = &details.offender; -// // Skip if the validator is invulnerable. -// if invulnerables.contains(&validator) { -// log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator); -// continue -// } - -// add_db_reads_writes(1, 0); -// let Some(exposure_overview) = >::get(&offence_era, validator) -// else { -// // defensive: this implies offence is for a discarded era, and should already be -// // filtered out. -// log!( -// warn, -// "🦹 on_offence: no exposure found for {:?} in era {}; ignoring offence", -// validator, -// offence_era -// ); -// continue; -// }; - -// Self::deposit_event(Event::::OffenceReported { -// validator: validator.clone(), -// fraction: *slash_fraction, -// offence_era, -// }); - -// if offence_era == active_era.index { -// // offence is in the current active era. Report it to session to maybe disable the -// // validator. -// add_db_reads_writes(2, 2); -// T::SessionInterface::report_offence( -// validator.clone(), -// OffenceSeverity(*slash_fraction), -// ); -// } -// add_db_reads_writes(1, 0); -// let prior_slash_fraction = ValidatorSlashInEra::::get(offence_era, validator) -// .map_or(Zero::zero(), |(f, _)| f); - -// add_db_reads_writes(1, 0); -// if let Some(existing) = OffenceQueue::::get(offence_era, validator) { -// if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() { -// add_db_reads_writes(0, 2); -// OffenceQueue::::insert( -// offence_era, -// validator, -// OffenceRecord { -// reporter: details.reporters.first().cloned(), -// reported_era: active_era.index, -// slash_fraction: *slash_fraction, -// ..existing -// }, -// ); - -// // update the slash fraction in the `ValidatorSlashInEra` storage. -// ValidatorSlashInEra::::insert( -// offence_era, -// validator, -// (slash_fraction, exposure_overview.own), -// ); - -// log!( -// debug, -// "🦹 updated slash for {:?}: {:?} (prior: {:?})", -// validator, -// slash_fraction, -// prior_slash_fraction, -// ); -// } else { -// log!( -// debug, -// "🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})", -// validator, -// slash_fraction, -// prior_slash_fraction, -// ); -// } -// } else if slash_fraction.deconstruct() > prior_slash_fraction.deconstruct() { -// add_db_reads_writes(0, 3); -// ValidatorSlashInEra::::insert( -// offence_era, -// validator, -// (slash_fraction, exposure_overview.own), -// ); - -// OffenceQueue::::insert( -// offence_era, -// validator, -// OffenceRecord { -// reporter: details.reporters.first().cloned(), -// reported_era: active_era.index, -// // there are cases of validator with no exposure, hence 0 page, so we -// // saturate to avoid underflow. -// exposure_page: exposure_overview.page_count.saturating_sub(1), -// slash_fraction: *slash_fraction, -// prior_slash_fraction, -// }, -// ); - -// OffenceQueueEras::::mutate(|q| { -// if let Some(eras) = q { -// log!(debug, "🦹 inserting offence era {} into existing queue", offence_era); -// eras.binary_search(&offence_era) -// .err() -// .map(|idx| eras.try_insert(idx, offence_era).defensive()); -// } else { -// let mut eras = BoundedVec::default(); -// log!(debug, "🦹 inserting offence era {} into empty queue", offence_era); -// let _ = eras.try_push(offence_era).defensive(); -// *q = Some(eras); -// } -// }); - -// log!( -// debug, -// "🦹 queued slash for {:?}: {:?} (prior: {:?})", -// validator, -// slash_fraction, -// prior_slash_fraction, -// ); -// } else { -// log!( -// debug, -// "🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})", -// validator, -// slash_fraction, -// prior_slash_fraction, -// ); -// } -// } - -// consumed_weight -// } -// } impl ScoreProvider for Pallet { type Score = VoteWeight; From e8c75cb3b12507ed41bd41f6638f94effd262a61 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Thu, 27 Feb 2025 12:04:32 +0200 Subject: [PATCH 09/10] Notify pallet_session for offences from ah-client --- substrate/frame/staking/ah-client/src/lib.rs | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index 9278cb1303af2..f44d307184887 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -77,7 +77,10 @@ pub mod pallet { use polkadot_primitives::Id as ParaId; use polkadot_runtime_parachains::origin::{ensure_parachain, Origin}; use sp_runtime::Perbill; - use sp_staking::{offence::OnOffenceHandler, SessionIndex}; + use sp_staking::{ + offence::{OffenceSeverity, OnOffenceHandler}, + SessionIndex, + }; const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); @@ -249,18 +252,22 @@ pub mod pallet { slash_fraction: &[Perbill], slash_session: SessionIndex, ) -> Weight { - let offenders_and_slashes = offenders - .iter() - .cloned() - .zip(slash_fraction) - .map(|(offence, fraction)| { - Offence::new( - offence.offender.0.into(), - offence.reporters.into_iter().map(|r| r.into()).collect(), - *fraction, - ) - }) - .collect::>(); + let mut offenders_and_slashes = Vec::new(); + + // notify pallet-session about the offences + for (offence, fraction) in offenders.iter().cloned().zip(slash_fraction) { + >::report_offence( + offence.offender.0.clone(), + OffenceSeverity(*fraction), + ); + + // prepare an `Offence` instance for the XCM message + offenders_and_slashes.push(Offence::new( + offence.offender.0.into(), + offence.reporters.into_iter().map(|r| r.into()).collect(), + *fraction, + )); + } // send the offender immediately over xcm let message = Xcm(vec![ From bfc05762016fe5ca2b22d1de5c5792609ef5283b Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov Date: Fri, 28 Feb 2025 10:00:55 +0200 Subject: [PATCH 10/10] Quick and dirty fixes to get rid of the compilation errors (TODOs added) --- polkadot/runtime/westend/src/lib.rs | 2 +- substrate/frame/staking/ah-client/src/lib.rs | 16 ++++++-- substrate/frame/staking/rc-client/src/lib.rs | 22 ++++++++++ substrate/frame/staking/src/lib.rs | 25 ------------ substrate/frame/staking/src/migrations.rs | 1 + substrate/frame/staking/src/pallet/impls.rs | 42 ++++++++++---------- substrate/frame/staking/src/pallet/mod.rs | 3 +- 7 files changed, 59 insertions(+), 52 deletions(-) diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index ac41547de5fdb..eaa8cada34420 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -763,7 +763,7 @@ impl pallet_staking::Config for Runtime { type BondingDuration = BondingDuration; type SlashDeferDuration = SlashDeferDuration; type AdminOrigin = EitherOf, StakingAdmin>; - type SessionInterface = Self; + type SessionInterface = (); // Should be pallet_staking_rc_client on ah-next type EraPayout = EraPayout; type MaxExposurePageSize = MaxExposurePageSize; type NextNewSession = Session; diff --git a/substrate/frame/staking/ah-client/src/lib.rs b/substrate/frame/staking/ah-client/src/lib.rs index f44d307184887..a265aa025250d 100644 --- a/substrate/frame/staking/ah-client/src/lib.rs +++ b/substrate/frame/staking/ah-client/src/lib.rs @@ -154,9 +154,19 @@ pub mod pallet { fn new_session( _: sp_staking::SessionIndex, ) -> Option::AccountId, ())>> { - // If there is a new validator set - return it. Otherwise return `None`. - ValidatorSet::::take() - .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) + let maybe_new_validator_set = ValidatorSet::::take() + .map(|validators| validators.into_iter().map(|v| (v, ())).collect()); + + // A new validator set is an indication for a new era. Clear + if maybe_new_validator_set.is_none() { + // TODO: historical sessions should be pruned. This used to happen after the bonding + // period for the session but it would be nice to avoid XCM messages for prunning + // and trigger it from RC directly. + + // >::prune_up_to(up_to); // TODO!!! + } + + return maybe_new_validator_set } fn new_session_genesis( diff --git a/substrate/frame/staking/rc-client/src/lib.rs b/substrate/frame/staking/rc-client/src/lib.rs index 7fadd15bcc12f..4f38541c324a1 100644 --- a/substrate/frame/staking/rc-client/src/lib.rs +++ b/substrate/frame/staking/rc-client/src/lib.rs @@ -50,6 +50,21 @@ pub trait StakingApi { fn reward_by_ids(validators_points: Vec<(AccountId32, u32)>); } +// TODO: update the comment +/// Means for interacting with a specialized version of the `session` trait. +/// +/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config` +pub trait SessionInterface { + /// Get the validators from session. + fn validators() -> Vec; +} + +impl SessionInterface for () { + fn validators() -> Vec { + Vec::new() + } +} + /// `pallet-staking-ah-client` pallet index on Relay chain. Used to construct remote calls. /// /// The codec index must correspond to the index of `pallet-staking-ah-client` in the @@ -185,6 +200,13 @@ pub mod pallet { } } + impl SessionInterface for Pallet { + fn validators() -> Vec { + // TODO: cache the last applied validator set and return it here + vec![] + } + } + fn mk_relay_chain_call(call: SessionCalls) -> Instruction<()> { Instruction::Transact { origin_kind: OriginKind::Superuser, diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index ca0ef3058edb8..20a561271ead6 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -890,31 +890,6 @@ impl NominationsQuota for FixedNominationsQuot } } -/// Means for interacting with a specialized version of the `session` trait. -/// -/// This is needed because `Staking` sets the `ValidatorIdOf` of the `pallet_session::Config` -pub trait SessionInterface { - /// Report an offending validator. - fn report_offence(validator: AccountId, severity: OffenceSeverity); - /// Get the validators from session. - fn validators() -> Vec; - /// Prune historical session tries up to but not including the given index. - fn prune_historical_up_to(up_to: SessionIndex); -} - -// TODO: Why we need this? -impl SessionInterface for () { - fn report_offence(_validator: AccountId, _severity: OffenceSeverity) { - () - } - fn validators() -> Vec { - Vec::new() - } - fn prune_historical_up_to(_: SessionIndex) { - () - } -} - /// Handler for determining how much of a balance should be paid out on the current era. pub trait EraPayout { /// Determine the payout for this era. diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index 5b0118da67ef7..53a86506f98b2 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -264,6 +264,7 @@ pub mod v16 { /// Migrating `OffendingValidators` from `Vec<(u32, bool)>` to `Vec` pub mod v15 { use super::*; + use pallet_staking_rc_client::SessionInterface; // The disabling strategy used by staking pallet type DefaultDisablingStrategy = pallet_session::disabling::UpToLimitDisablingStrategy; diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 2c0e18d11b801..64ac2b6f6a323 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -32,7 +32,7 @@ use frame_support::{ weights::Weight, }; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; -use pallet_staking_rc_client::{Offence, StakingApi}; +use pallet_staking_rc_client::{Offence, SessionInterface, StakingApi}; use sp_core::crypto::AccountId32; use sp_runtime::{ traits::{Bounded, CheckedAdd, Convert, SaturatedConversion, Saturating, StaticLookup, Zero}, @@ -50,8 +50,8 @@ use crate::{ asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, BoundedExposuresOf, EraInfo, EraPayout, Exposure, Forcing, IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, MaxWinnersPerPageOf, Nominations, - NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, SnapshotStatus, - StakingLedger, ValidatorPrefs, STAKING_ID, + NominationsQuota, PositiveImbalanceOf, RewardDestination, SnapshotStatus, StakingLedger, + ValidatorPrefs, STAKING_ID, }; use alloc::{boxed::Box, vec, vec::Vec}; @@ -573,9 +573,10 @@ impl Pallet { slashing::clear_era_metadata::(pruned_era); } - if let Some(&(_, first_session)) = bonded.first() { - T::SessionInterface::prune_historical_up_to(first_session); - } + // TODO: should be handled in ah_client (see the comment there) + // if let Some(&(_, first_session)) = bonded.first() { + // T::SessionInterface::prune_historical_up_to(first_session); + // } } }); } @@ -1743,8 +1744,7 @@ where } add_db_reads_writes(1, 0); - let Some(exposure_overview) = - >::get(&offence_era, &validator) + let Some(exposure_overview) = >::get(&offence_era, &validator) else { // defensive: this implies offence is for a discarded era, and should already be // filtered out. @@ -1763,19 +1763,19 @@ where offence_era, }); - if offence_era == active_era.index { - // offence is in the current active era. Report it to session to maybe disable the - // validator. - add_db_reads_writes(2, 2); - T::SessionInterface::report_offence( - validator.clone(), - OffenceSeverity(slash_fraction), - ); - } + // TODO: handled in ah_client. Verify it works correctly before removing the commented + // out code. if offence_era == active_era.index { + // // offence is in the current active era. Report it to session to maybe disable the + // // validator. + // add_db_reads_writes(2, 2); + // T::SessionInterface::report_offence( + // validator.clone(), + // OffenceSeverity(slash_fraction), + // ); + // } add_db_reads_writes(1, 0); - let prior_slash_fraction = - ValidatorSlashInEra::::get(offence_era, &validator) - .map_or(Zero::zero(), |(f, _)| f); + let prior_slash_fraction = ValidatorSlashInEra::::get(offence_era, &validator) + .map_or(Zero::zero(), |(f, _)| f); add_db_reads_writes(1, 0); if let Some(existing) = OffenceQueue::::get(offence_era, &validator) { @@ -1903,8 +1903,6 @@ where // } // } - - impl ScoreProvider for Pallet { type Score = VoteWeight; diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 438b8c0e344fb..247eba25ba022 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -35,6 +35,7 @@ use frame_support::{ BoundedBTreeSet, BoundedVec, }; use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; +use pallet_staking_rc_client::SessionInterface; use rand::seq::SliceRandom; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, @@ -60,7 +61,7 @@ use crate::{ asset, slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, EraRewardPoints, ExposurePage, Forcing, LedgerIntegrityState, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, - SessionInterface, StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, + StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, }; // The speculative number of spans are used as an input of the weight annotation of