diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 6901278dbb2f6..efa8bdd141300 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -323,8 +323,8 @@ impl pallet_session::Config for Runtime { } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } pallet_staking_reward_curve::build! { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index f63ee8e4fb20a..80d3fa139bbe8 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -537,8 +537,8 @@ impl pallet_session::Config for Runtime { } impl pallet_session::historical::Config for Runtime { - type FullIdentification = pallet_staking::Exposure; - type FullIdentificationOf = pallet_staking::ExposureOf; + type FullIdentification = pallet_staking::ExistenceOrLegacyExposure; + type FullIdentificationOf = pallet_staking::ExistenceOrLegacyExposureOf; } pub struct MaybeSignedPhase; diff --git a/prdoc/pr_7936.prdoc b/prdoc/pr_7936.prdoc new file mode 100644 index 0000000000000..946de516acb83 --- /dev/null +++ b/prdoc/pr_7936.prdoc @@ -0,0 +1,55 @@ +title: 'Replace Validator FullIdentification from `Exposure` to `Existence`' +doc: +- audience: Runtime Dev + description: |- + This introduces a new type in `pallet-staking`, `ExistenceOf`, which replaces `ExposureOf`. + With this change, runtimes can be configured to identify a validator solely by their presence, + rather than using full exposure data. + + This is particularly useful when configuring historical sessions, for example: + + ```rust + impl pallet_session::historical::Config for Runtime { + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; + } + ``` + + However, for existing runtimes that depend on the `Exposure` type for `pallet-offences` - often configured like this: + + ```rust + impl pallet_offences::Config for Runtime { + ... + type IdentificationTuple = pallet_session::historical::IdentificationTuple; + } + ``` + + Where `IdentificationTuple` is defined as: + ```rust + pub type IdentificationTuple = (::ValidatorId, ::FullIdentification); + ``` + + You should use `ExistenceOrLegacyExposureOf` instead. This type includes a custom encoder/decoder that supports both + the legacy `Exposure` type and the new `Existence` type. + + This compatibility layer is necessary because `pallet-offences` stores the `FullIdentification` type in its storage. + If you replace `FullIdentification` with `Existence` directly, any previously stored items using `Exposure` will + fail to decode. `ExistenceOrLegacyExposureOf` ensures backward compatibility after this change. + +crates: +- name: pallet-babe + bump: patch +- name: pallet-beefy + bump: patch +- name: pallet-grandpa + bump: patch +- name: pallet-offences-benchmarking + bump: patch +- name: pallet-root-offences + bump: patch +- name: pallet-session-benchmarking + bump: patch +- name: pallet-staking + bump: major +- name: westend-runtime + bump: minor diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 3a0c2e13c823f..d0a9c667fbb23 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -693,8 +693,8 @@ impl pallet_session::Config for Runtime { } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } pallet_staking_reward_curve::build! { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index c51ddeb9ab9cd..cfbbbf5dd0e18 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -105,8 +105,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index ee84d9e5bbe40..5df910541b301 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -191,8 +191,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = pallet_staking::Exposure; - type FullIdentificationOf = pallet_staking::ExposureOf; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs index 8619325d56b35..4b02fd6ca0337 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs @@ -147,8 +147,8 @@ impl pallet_session::Config for Runtime { type WeightInfo = (); } impl pallet_session::historical::Config for Runtime { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } frame_election_provider_support::generate_solution_type!( diff --git a/substrate/frame/grandpa/src/mock.rs b/substrate/frame/grandpa/src/mock.rs index 5de6107440d97..6c3870a90079c 100644 --- a/substrate/frame/grandpa/src/mock.rs +++ b/substrate/frame/grandpa/src/mock.rs @@ -109,8 +109,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } impl pallet_authorship::Config for Test { diff --git a/substrate/frame/offences/benchmarking/src/mock.rs b/substrate/frame/offences/benchmarking/src/mock.rs index fe5ef8e172c81..daa0b6c85bba9 100644 --- a/substrate/frame/offences/benchmarking/src/mock.rs +++ b/substrate/frame/offences/benchmarking/src/mock.rs @@ -53,8 +53,8 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } sp_runtime::impl_opaque_keys! { diff --git a/substrate/frame/root-offences/src/lib.rs b/substrate/frame/root-offences/src/lib.rs index 8e91c4ecfd1cd..33a3371d2d263 100644 --- a/substrate/frame/root-offences/src/lib.rs +++ b/substrate/frame/root-offences/src/lib.rs @@ -49,8 +49,8 @@ pub mod pallet { + pallet_staking::Config + pallet_session::Config::AccountId> + pallet_session::historical::Config< - FullIdentification = (), - FullIdentificationOf = pallet_staking::NullIdentity, + FullIdentification = pallet_staking::Existence, + FullIdentificationOf = pallet_staking::ExistenceOf, > { type RuntimeEvent: From> + IsType<::RuntimeEvent>; diff --git a/substrate/frame/root-offences/src/mock.rs b/substrate/frame/root-offences/src/mock.rs index 19119c5541e57..cd409dfd4ec9b 100644 --- a/substrate/frame/root-offences/src/mock.rs +++ b/substrate/frame/root-offences/src/mock.rs @@ -144,8 +144,8 @@ impl pallet_staking::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } sp_runtime::impl_opaque_keys! { diff --git a/substrate/frame/session/benchmarking/src/mock.rs b/substrate/frame/session/benchmarking/src/mock.rs index da39ed4e1ffd0..40e7215ccc307 100644 --- a/substrate/frame/session/benchmarking/src/mock.rs +++ b/substrate/frame/session/benchmarking/src/mock.rs @@ -67,8 +67,8 @@ impl pallet_timestamp::Config for Test { type WeightInfo = (); } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = pallet_staking::NullIdentity; + type FullIdentification = pallet_staking::Existence; + type FullIdentificationOf = pallet_staking::ExistenceOf; } sp_runtime::impl_opaque_keys! { diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 6d712f1329148..75d0a76b7db7f 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -308,7 +308,9 @@ mod pallet; extern crate alloc; use alloc::{collections::btree_map::BTreeMap, vec, vec::Vec}; -use codec::{Decode, DecodeWithMemTracking, Encode, HasCompact, MaxEncodedLen}; +use codec::{ + Decode, DecodeWithMemTracking, Encode, EncodeLike, HasCompact, Input, MaxEncodedLen, Output, +}; use frame_support::{ defensive, defensive_assert, traits::{ @@ -1066,8 +1068,10 @@ impl Convert> for StashOf { /// /// Active exposure is the exposure of the validator set currently validating, i.e. in /// `active_era`. It can differ from the latest planned exposure in `current_era`. +#[deprecated(note = "Use `ExistenceOf` or `ExistenceOrLegacyExposureOf` instead")] pub struct ExposureOf(core::marker::PhantomData); +#[allow(deprecated)] impl Convert>>> for ExposureOf { @@ -1077,10 +1081,69 @@ impl Convert } } -pub struct NullIdentity; -impl Convert> for NullIdentity { - fn convert(_: T) -> Option<()> { - Some(()) +/// A type representing the presence of a validator. Encodes as a unit type. +pub type Existence = (); + +/// A converter type that returns `Some(())` if the validator exists in the current active era, +/// otherwise `None`. This serves as a lightweight presence check for validators. +pub struct ExistenceOf(core::marker::PhantomData); +impl Convert> for ExistenceOf { + fn convert(validator: T::AccountId) -> Option { + Validators::::contains_key(&validator).then_some(()) + } +} + +/// A compatibility wrapper type used to represent the presence of a validator in the current era. +/// Encodes as type [`Existence`] but can decode from legacy [`Exposure`] values for backward +/// compatibility. +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, RuntimeDebug, TypeInfo, DecodeWithMemTracking)] +pub enum ExistenceOrLegacyExposure { + /// Validator exists in the current era. + Exists, + /// Legacy `Exposure` data, retained for decoding compatibility. + Exposure(Exposure), +} + +/// Converts a validator account ID to a Some([`ExistenceOrLegacyExposure::Exists`]) if the +/// validator exists in the current era, otherwise `None`. +pub struct ExistenceOrLegacyExposureOf(core::marker::PhantomData); + +impl Convert>>> + for ExistenceOrLegacyExposureOf +{ + fn convert( + validator: T::AccountId, + ) -> Option>> { + ActiveEra::::get() + .map(|active_era| ErasStakersOverview::::contains_key(active_era.index, &validator)) + .unwrap_or(false) + .then_some(ExistenceOrLegacyExposure::Exists) + } +} + +impl Encode for ExistenceOrLegacyExposure +where + Exposure: Encode, +{ + fn encode_to(&self, dest: &mut T) { + match self { + ExistenceOrLegacyExposure::Exists => (), + ExistenceOrLegacyExposure::Exposure(exposure) => exposure.encode_to(dest), + } + } +} + +impl EncodeLike for ExistenceOrLegacyExposure where Exposure: Encode {} + +impl Decode for ExistenceOrLegacyExposure +where + Exposure: Decode, +{ + fn decode(input: &mut I) -> Result { + match input.remaining_len() { + Ok(Some(x)) if x > 0 => Ok(ExistenceOrLegacyExposure::Exposure(Decode::decode(input)?)), + _ => Ok(ExistenceOrLegacyExposure::Exists), + } } } @@ -1369,3 +1432,52 @@ impl BenchmarkingConfig for TestBenchmarkingConfig { type MaxValidators = frame_support::traits::ConstU32<100>; type MaxNominators = frame_support::traits::ConstU32<100>; } + +#[cfg(test)] +mod test { + use crate::ExistenceOrLegacyExposure; + use codec::{Decode, Encode}; + use sp_staking::{Exposure, IndividualExposure}; + + #[test] + fn existence_encodes_decodes_correctly() { + let encoded_existence = ExistenceOrLegacyExposure::::Exists.encode(); + assert!(encoded_existence.is_empty()); + + // try decoding the existence + let decoded_existence = + ExistenceOrLegacyExposure::::decode(&mut encoded_existence.as_slice()) + .unwrap(); + assert!(matches!(decoded_existence, ExistenceOrLegacyExposure::Exists)); + + // check that round-trip encoding works + assert_eq!(encoded_existence, decoded_existence.encode()); + } + + #[test] + fn legacy_existence_encodes_decodes_correctly() { + let legacy_exposure = Exposure:: { + total: 1, + own: 2, + others: vec![IndividualExposure { who: 3, value: 4 }], + }; + + let encoded_legacy_exposure = legacy_exposure.encode(); + + // try decoding the legacy exposure + let decoded_legacy_exposure = + ExistenceOrLegacyExposure::::decode(&mut encoded_legacy_exposure.as_slice()) + .unwrap(); + assert_eq!( + decoded_legacy_exposure, + ExistenceOrLegacyExposure::Exposure(Exposure { + total: 1, + own: 2, + others: vec![IndividualExposure { who: 3, value: 4 }] + }) + ); + + // round trip encoding works + assert_eq!(encoded_legacy_exposure, decoded_legacy_exposure.encode()); + } +} diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index a759d4ab2c63f..49e8b9705f9f0 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -151,8 +151,8 @@ impl pallet_session::Config for Test { } impl pallet_session::historical::Config for Test { - type FullIdentification = (); - type FullIdentificationOf = NullIdentity; + type FullIdentification = Existence; + type FullIdentificationOf = ExistenceOf; } impl pallet_authorship::Config for Test { type FindAuthor = Author11; diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 12af97385486d..1dfd88316801b 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -50,9 +50,10 @@ use sp_staking::{ use crate::{ asset, election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, - BalanceOf, EraInfo, EraPayout, Exposure, Forcing, IndividualExposure, LedgerIntegrityState, - MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, - RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, STAKING_ID, + BalanceOf, EraInfo, EraPayout, Existence, ExistenceOrLegacyExposure, Exposure, Forcing, + IndividualExposure, LedgerIntegrityState, MaxNominationsOf, MaxWinnersOf, Nominations, + NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger, + ValidatorPrefs, STAKING_ID, }; use alloc::{boxed::Box, vec, vec::Vec}; @@ -1428,7 +1429,7 @@ impl ElectionDataProvider for Pallet { // We can't handle this case yet -- return an error. WIP to improve handling this case in // . - if bounds.exhausted(None, CountBound(T::TargetList::count() as u32).into()) { + if bounds.exhausted(None, CountBound(T::TargetList::count()).into()) { return Err("Target snapshot too big") } @@ -1571,42 +1572,23 @@ impl pallet_session::SessionManager for Pallet { } } -impl historical::SessionManager>> +impl + historical::SessionManager>> for Pallet { fn new_session( new_index: SessionIndex, - ) -> Option>)>> { + ) -> 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() + validators.into_iter().map(|v| (v, ExistenceOrLegacyExposure::Exists)).collect() }) } fn new_session_genesis( new_index: SessionIndex, - ) -> Option>)>> { + ) -> 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() + validators.into_iter().map(|v| (v, ExistenceOrLegacyExposure::Exists)).collect() }, ) } @@ -1618,12 +1600,12 @@ impl historical::SessionManager historical::SessionManager for Pallet { - fn new_session(new_index: SessionIndex) -> Option> { +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> { + fn new_session_genesis(new_index: SessionIndex) -> Option> { >::new_session_genesis(new_index) .map(|validators| validators.into_iter().map(|v| (v, ())).collect()) } diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 1b3411b65360d..20b9b55c8f69d 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -4662,6 +4662,28 @@ fn restricted_accounts_can_only_withdraw() { }) } +#[test] +fn validator_existence_check() { + ExtBuilder::default().build_and_execute(|| { + mock::start_active_era(1); + + // Given: 11 is an active validator for Era 1 + assert!(ErasStakersOverview::::get(1, 11).is_some()); + + // And: 31 is not an active validator for Era 1 + assert!(ErasStakersOverview::::get(1, 31).is_none()); + + // Then: 11 converts to Exists. + assert_eq!( + ExistenceOrLegacyExposureOf::::convert(11), + Some(ExistenceOrLegacyExposure::Exists) + ); + + // And: 31 converts to None (Not Exists). + assert_eq!(ExistenceOrLegacyExposureOf::::convert(31), None); + }); +} + mod election_data_provider { use super::*; use frame_election_provider_support::ElectionDataProvider;