feat: forward session events to staking pallet for era progression on Asset Hub

Asset Hub's pallet_staking_async era never advances because it expects
SessionReport messages from the relay chain's ah_client pallet, which is
not yet active. This adds a StakingSessionManager wrapper that intercepts
local session rotation events and generates SessionReport messages to drive
era transitions directly on Asset Hub.

Changes:
- Add StakingSessionManager in staking.rs that delegates to both
  CollatorSelection and Staking via on_relay_session_report()
- Switch SessionManager from CollatorSelection to StakingSessionManager
- Add FixActiveEraStart migration to correct ActiveEra.start=0 from genesis
- Bump spec_version to 1_020_002
This commit is contained in:
2026-02-12 23:58:58 +03:00
parent c29059b09b
commit 3514625d25
2 changed files with 81 additions and 2 deletions
@@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
impl_name: alloc::borrow::Cow::Borrowed("asset-hub-pezkuwichain"),
authoring_version: 1,
spec_version: 1_020_001,
spec_version: 1_020_002,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 16,
@@ -844,7 +844,7 @@ impl pezpallet_session::Config for Runtime {
type ValidatorIdOf = pezpallet_collator_selection::IdentityCollator;
type ShouldEndSession = pezpallet_session::PeriodicSessions<Period, Offset>;
type NextSessionRotation = pezpallet_session::PeriodicSessions<Period, Offset>;
type SessionManager = CollatorSelection;
type SessionManager = StakingSessionManager;
// Essentially just Aura, but let's be pedantic.
type SessionHandler = <SessionKeys as pezsp_runtime::traits::OpaqueKeys>::KeyTypeIdProviders;
type Keys = SessionKeys;
@@ -1408,8 +1408,33 @@ pub type TxExtension = pezcumulus_pezpallet_weight_reclaim::StorageWeightReclaim
/// Unchecked extrinsic type as expected by this runtime.
pub type UncheckedExtrinsic =
generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, TxExtension>;
/// One-time migration to fix ActiveEra.start which was set to 0 at genesis.
/// Without this, the first era's duration would be calculated as (now - 0) = ~56 years,
/// though MaxEraDuration caps it to 6 hours. This migration sets it to the current timestamp
/// so the first era duration is calculated correctly from the upgrade moment.
pub struct FixActiveEraStart;
impl pezframe_support::traits::OnRuntimeUpgrade for FixActiveEraStart {
fn on_runtime_upgrade() -> Weight {
let now_ms = pezpallet_timestamp::Now::<Runtime>::get();
if now_ms > 0 {
pezpallet_staking_async::ActiveEra::<Runtime>::mutate(|era| {
if let Some(ref mut info) = era {
info.start = Some(now_ms);
log::info!(
target: "runtime::staking",
"FixActiveEraStart: Set ActiveEra.start to {}",
now_ms,
);
}
});
}
<Runtime as pezframe_system::Config>::DbWeight::get().reads_writes(2, 1)
}
}
/// Migrations to apply on runtime upgrade.
pub type Migrations = (
FixActiveEraStart,
InitStorageVersions,
// unreleased
pezcumulus_pezpallet_xcmp_queue::migration::v4::MigrationToV4<Runtime>,
@@ -312,6 +312,60 @@ impl pezpallet_staking_async_rc_client::Config for Runtime {
type MaxValidatorSetRetries = ConstU32<64>;
}
/// Forwards session events to both CollatorSelection (collator management) and
/// Staking pallet (era management) via local SessionReport generation.
///
/// This is needed because `pallet_staking_async` expects `SessionReport` messages from
/// the relay chain's `ah_client` pallet, which is not yet active. This wrapper generates
/// local session reports from AH's own session rotation events.
pub struct StakingSessionManager;
impl pezpallet_session::SessionManager<AccountId> for StakingSessionManager {
fn new_session(new_index: u32) -> Option<Vec<AccountId>> {
<CollatorSelection as pezpallet_session::SessionManager<AccountId>>::new_session(new_index)
}
fn end_session(end_index: u32) {
// Forward to CollatorSelection first
<CollatorSelection as pezpallet_session::SessionManager<AccountId>>::end_session(end_index);
// Build local SessionReport for staking era progression
let current_era =
pezpallet_staking_async::CurrentEra::<Runtime>::get().unwrap_or(0);
let active_era_idx = pezpallet_staking_async::ActiveEra::<Runtime>::get()
.map(|e| e.index)
.unwrap_or(0);
// Provide activation_timestamp when a planned era exists (CurrentEra > ActiveEra)
let activation_timestamp = if current_era > active_era_idx {
let now_ms = pezpallet_timestamp::Now::<Runtime>::get();
Some((now_ms, current_era))
} else {
None
};
// Equal reward points for all validators
let validator_points: Vec<(AccountId, u32)> =
pezpallet_staking_async::Validators::<Runtime>::iter_keys()
.map(|v| (v, 20u32))
.collect();
let report = rc_client::SessionReport::new_terminal(
end_index,
validator_points,
activation_timestamp,
);
let _ = <Staking as rc_client::AHStakingInterface>::on_relay_session_report(report);
}
fn start_session(start_index: u32) {
<CollatorSelection as pezpallet_session::SessionManager<AccountId>>::start_session(
start_index,
);
}
}
#[derive(Encode, Decode)]
// Call indices taken from zagros-next runtime.
pub enum RelayChainRuntimePallets {