From 4d0176e2f0501a04313b2183c355c48abeef3287 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Wed, 18 Feb 2026 21:22:56 +0300 Subject: [PATCH] fix(staking-async): add stalled era recovery for zombie pending eras When an election completes with 0 winners and RC never sends activation_timestamp, the pending era becomes a zombie blocking all future era transitions. Detect this condition (election idle + not fetching) and revert the planned era to break the deadlock. --- .../staking-async/src/pezpallet/mod.rs | 3 ++ .../staking-async/src/session_rotation.rs | 37 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs b/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs index 2799fbf8..7603e9fc 100644 --- a/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs +++ b/bizinikiwi/pezframe/staking-async/src/pezpallet/mod.rs @@ -1226,6 +1226,9 @@ pub mod pezpallet { EraDurationBoundExceeded, /// Received a validator activation event that is not recognized. UnknownValidatorActivation, + /// A pending era's election completed but produced no viable result. The planned + /// era was reverted and a new election was initiated to break the deadlock. + StalledEraRecovery, } #[pezpallet::error] diff --git a/bizinikiwi/pezframe/staking-async/src/session_rotation.rs b/bizinikiwi/pezframe/staking-async/src/session_rotation.rs index 3c66fdc2..bf8998e8 100644 --- a/bizinikiwi/pezframe/staking-async/src/session_rotation.rs +++ b/bizinikiwi/pezframe/staking-async/src/session_rotation.rs @@ -673,13 +673,36 @@ impl Rotator { Self::plan_new_era(); }, (true, true) => { - // we are waiting for to start the previously planned era, we cannot plan a new era - // now. - crate::log!( - debug, - "time to plan a new era {:?}, but waiting for the activation of the previous.", - current_planned_era - ); + // We have a pending era but also want to plan a new one. + // Detect zombie pending era: election completed but produced 0 winners, + // RC never sent activation_timestamp. Break the deadlock by reverting + // the planned era and re-planning with a fresh election. + let election_idle = T::ElectionProvider::status().is_err(); + let not_fetching = NextElectionPage::::get().is_none(); + if election_idle && not_fetching { + crate::log!( + warn, + "Detected stalled pending era {:?}: election finished but era was \ + never activated. Reverting planned era and re-planning.", + current_planned_era + ); + let active = Self::active_era(); + CurrentEra::::put(active); + EraElectionPlanner::::cleanup(); + Pezpallet::::deposit_event(Event::Unexpected( + UnexpectedKind::StalledEraRecovery, + )); + Self::plan_new_era(); + } else { + crate::log!( + debug, + "time to plan a new era {:?}, but waiting for the activation of \ + the previous (election_idle: {}, fetching: {}).", + current_planned_era, + election_idle, + !not_fetching + ); + } }, }