diff --git a/prdoc/pr_11647.prdoc b/prdoc/pr_11647.prdoc new file mode 100644 index 0000000000000..ff6d770cbef3e --- /dev/null +++ b/prdoc/pr_11647.prdoc @@ -0,0 +1,15 @@ +title: 'Fix try-state warning for LastValidatorEra in staking-async' +doc: +- audience: Runtime Dev + description: |- + Fixes a false try-state warning where `LastValidatorEra` was flagged as incorrect for active + validators. After the election for the next era completes but before that era becomes active, + `LastValidatorEra` is correctly set to `active_era + 1`. The previous check only accepted + `active_era`, causing spurious warnings. + + Adds a test verifying `LastValidatorEra` transitions from `active_era` to `active_era + 1` + once the next era's election results are stored. + +crates: +- name: pallet-staking-async + bump: patch diff --git a/substrate/frame/staking-async/src/pallet/impls.rs b/substrate/frame/staking-async/src/pallet/impls.rs index 07314b0087565..f4c6e1f6f4117 100644 --- a/substrate/frame/staking-async/src/pallet/impls.rs +++ b/substrate/frame/staking-async/src/pallet/impls.rs @@ -2011,14 +2011,16 @@ impl Pallet { let Some(era) = ActiveEra::::get().map(|a| a.index) else { return Ok(()) }; let overview_and_pages = ErasStakersOverview::::iter_prefix(era) .map(|(validator, metadata)| { - // ensure `LastValidatorEra` is correctly set - if LastValidatorEra::::get(&validator) != Some(era) { + let last_validator_era = LastValidatorEra::::get(&validator).unwrap_or_default(); + // If election for next era is finished, last_validator_era is set to next era. + if last_validator_era != era && last_validator_era != era + 1 { log!( - warn, - "Validator {:?} has incorrect LastValidatorEra (expected {:?}, got {:?})", + error, + "Validator {:?} has incorrect LastValidatorEra (expected {:?} or {:?}, got {:?})", validator, era, - LastValidatorEra::::get(&validator) + era + 1, + last_validator_era ); } diff --git a/substrate/frame/staking-async/src/tests/try_state.rs b/substrate/frame/staking-async/src/tests/try_state.rs index d1d0111ea18f7..786467595af2e 100644 --- a/substrate/frame/staking-async/src/tests/try_state.rs +++ b/substrate/frame/staking-async/src/tests/try_state.rs @@ -88,6 +88,36 @@ fn try_state_bad_exposure() { }); } +#[test] +fn last_validator_era_can_be_one_greater_than_active_era() { + // When the election for the next era has finished but the era is not yet active, + // `LastValidatorEra` is set to `active_era + 1`. + ExtBuilder::default().try_state(false).build_and_execute(|| { + Session::roll_until_active_era(1); + let era = active_era(); + + // Before election, `LastValidatorEra` equals the active era. + for (validator, _) in ErasStakersOverview::::iter_prefix(era) { + assert_eq!(LastValidatorEra::::get(&validator), Some(era)); + } + + // Roll session by session until the election for the next era has been stored, i.e. + // `ErasStakersOverview` for the next era is populated. + while ErasStakersOverview::::iter_prefix(era + 1).next().is_none() { + Session::roll_to_next_session(); + } + + // Election for era 2 is stored but era 2 is not yet active. + assert_eq!(active_era(), 1); + assert_eq!(current_era(), 2); + + // After election, `LastValidatorEra` is now 1 greater than active era. + for (validator, _) in ErasStakersOverview::::iter_prefix(era) { + assert_eq!(LastValidatorEra::::get(&validator), Some(era + 1)); + } + }); +} + #[test] fn try_state_bad_eras_total_stake() { ExtBuilder::default().try_state(false).build_and_execute(|| {