Skip to main content

rialo_stake_cache_interface/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared types for the Stake Cache.
5//!
6//! This crate provides types that are shared between `svm-execution` and
7//! `rialo-s-program-runtime`, allowing builtin programs to access and
8//! manipulate stake cache data during transaction execution.
9//!
10//! ## Reward Distribution Flow
11//!
12//! The reward distribution follows a specific flow:
13//!
14//! 1. **FreezeStakes**: At epoch boundary, push pending to frozen (creates epoch snapshot)
15//! 2. **DistributeRewards**: Creates EpochRewards account (initially inactive/queued)
16//! 3. **Activation**: When EpochRewards becomes active:
17//!    - `pop_front_and_merge_to_baseline()` is called
18//!    - frozen.front() is merged into baseline
19//!    - Rewards are calculated from baseline only
20//! 4. **Distribution**: Rewards distributed across partitions
21//! 5. **Completion**: EpochRewards marked inactive
22//!
23//! ## Reward Eligibility
24//!
25//! Stakes are eligible for rewards based on the following checks:
26//! - `activation_requested.is_some()` → stake was activated
27//! - deactivation not yet effective (timestamp-based check against epoch boundary)
28//! - `validator.is_some()` → stake is delegated to a validator
29//!
30//! ## Lookup Methods
31//!
32//! Different lookup methods for different use cases:
33//!
34//! - **From Pending** (`get_*_from_pending`): Includes next epoch changes
35//! - **From Last Frozen** (`get_*_from_last_frozen`): Current epoch's effective state
36//! - **From First Frozen** (`get_*_from_first_frozen`): Oldest pending rewards epoch
37//! - **From Baseline** (`get_*_from_baseline`): Post-merge state for reward calculation
38
39use std::{
40    collections::{HashMap, HashSet, VecDeque},
41    sync::{
42        atomic::{AtomicBool, Ordering},
43        RwLock,
44    },
45};
46
47use rayon::prelude::*;
48use rialo_s_account::ReadableAccount;
49use rialo_s_clock::Epoch;
50use rialo_s_pubkey::Pubkey;
51use rialo_s_type_overrides::sync::Arc;
52use rialo_stake_manager_interface::instruction::StakeInfo;
53// Re-export ValidatorInfo so downstream crates (e.g., rialo-s-program-runtime) can reference the
54// type without adding a direct dependency on rialo-validator-registry-interface.
55pub use rialo_validator_registry_interface::instruction::ValidatorInfo;
56
57/// PDA derivation helpers for self-bond accounts.
58pub mod pda;
59pub use pda::{derive_self_bond_address, derive_self_bond_address_with_bump, SELF_BOND_SEED};
60
61/// A stake account that passed `is_stake_eligible` for some epoch.
62#[derive(Debug, Clone)]
63pub struct EligibleStake {
64    pub stake_pubkey: Pubkey,
65    pub stake: StakeAccount,
66    pub validator_pubkey: Pubkey,
67}
68
69/// Eligibility predicate for a stake at a given epoch start timestamp.
70///
71/// Given a stake account and `epoch_timestamp` (the freeze timestamp
72/// marking the start of some epoch), returns `true` iff the stake is
73/// effectively active at that boundary and delegated to a validator. The
74/// stake itself need not live in that epoch's delta — it can have been
75/// resolved from any predecessor frozen snapshot (or baseline) via the
76/// layered stake-cache lookup; only the boundary marked by
77/// `epoch_timestamp` matters.
78///
79/// The three clauses encode different invariants:
80///
81/// 1. **`activation_requested.is_some()`** — activation has taken effect.
82///    `activation_requested`, when set, is always written with the
83///    *current* timestamp at the moment the activation request is
84///    processed. So if a stake with an `activation_requested` timestamp
85///    appears in an epoch's set, that timestamp must have been set
86///    before the epoch began, i.e. before `epoch_timestamp`, meaning
87///    activation was already effective throughout that epoch. The
88///    stored timestamp itself therefore need not be checked for
89///    eligibility.
90///
91/// 2. **`deactivation_requested` is `None` or `>= epoch_timestamp`** —
92///    deactivation has not yet taken effect. `deactivation_requested`,
93///    in contrast, can be written with a future timestamp —
94///    specifically, it is set to the stake's `lockup_end`, as if the
95///    deactivation were actually requested only at that point. So if
96///    a stake with a `deactivation_requested` timestamp appears in an
97///    epoch's set, that timestamp may be after that epoch's start,
98///    i.e. after `epoch_timestamp`, meaning the deactivation had
99///    effectively not even been requested when the epoch started. The
100///    stored timestamp must therefore be checked: the stake is still
101///    eligible iff `deactivation_requested >= epoch_timestamp`
102///    (boundary inclusive — a stake whose
103///    `deactivation_requested == epoch_timestamp` is still eligible).
104///
105/// 3. **`validator.is_some()`** — the stake is delegated to a validator.
106pub fn is_stake_eligible(stake: &StakeAccount, epoch_timestamp: u64) -> bool {
107    stake.data.activation_requested.is_some()
108        && stake
109            .data
110            .deactivation_requested
111            .is_none_or(|d| d >= epoch_timestamp)
112        && stake.data.validator.is_some()
113}
114
115/// Filter a list of stake accounts down to those eligible at `epoch_timestamp`.
116pub fn filter_eligible_stakes(
117    stake_accounts: Vec<(Pubkey, StakeAccount)>,
118    epoch_timestamp: u64,
119) -> Vec<EligibleStake> {
120    stake_accounts
121        .into_iter()
122        .filter_map(|(stake_pubkey, stake)| {
123            // Destructure `validator` first; the `?` here short-circuits the
124            // `validator.is_some()` clause of `is_stake_eligible`, so the
125            // remaining call only needs to check the activation/deactivation
126            // clauses.
127            let validator_pubkey = stake.data.validator?;
128            if !is_stake_eligible(&stake, epoch_timestamp) {
129                return None;
130            }
131            Some(EligibleStake {
132                stake_pubkey,
133                stake,
134                validator_pubkey,
135            })
136        })
137        .collect()
138}
139
140/// A cache of stake and validator accounts.
141///
142/// This wraps `StakeCacheData` in `Arc<RwLock<...>>` to allow thread-safe shared
143/// access during parallel transaction execution. The Arc allows the same data
144/// to be shared between the Bank and StakesHandle, so mutations to pending
145/// stake data by builtin programs are visible to the Bank.
146#[derive(Debug, Clone)]
147pub struct StakeCache(Arc<RwLock<StakeCacheData>>);
148
149impl Default for StakeCache {
150    fn default() -> Self {
151        Self(Arc::new(RwLock::new(StakeCacheData::default())))
152    }
153}
154
155impl StakeCache {
156    /// Create a new empty stake cache.
157    pub fn new() -> Self {
158        Self::default()
159    }
160
161    /// Create a stake cache with the given data.
162    pub fn with_data(data: StakeCacheData) -> Self {
163        Self(Arc::new(RwLock::new(data)))
164    }
165
166    /// Create a stake cache from an existing Arc (for sharing references).
167    pub fn from_arc(arc: Arc<RwLock<StakeCacheData>>) -> Self {
168        Self(arc)
169    }
170
171    /// Get a clone of the inner Arc for sharing.
172    pub fn arc_clone(&self) -> Arc<RwLock<StakeCacheData>> {
173        Arc::clone(&self.0)
174    }
175
176    /// Acquire a read lock on the inner data.
177    pub fn read(&self) -> std::sync::RwLockReadGuard<'_, StakeCacheData> {
178        self.0.read().expect("Failed to acquire read lock")
179    }
180
181    /// Acquire a write lock on the inner data.
182    pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, StakeCacheData> {
183        self.0.write().expect("Failed to acquire write lock")
184    }
185
186    /// Get a stake account by pubkey.
187    ///
188    /// Note: This is a single-layer lookup on just this cache.
189    /// For layered lookup across baseline/frozen/pending, use `StakesHandle::get_stake_account`.
190    pub fn get_stake_account(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
191        let data = self.read();
192        data.stake_accounts.get(pubkey).and_then(|opt| opt.clone())
193    }
194
195    /// Get a validator account by pubkey.
196    ///
197    /// Note: This is a single-layer lookup on just this cache.
198    /// For layered lookup across baseline/frozen/pending, use `StakesHandle::get_validator_account`.
199    pub fn get_validator_account(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
200        let data = self.read();
201        data.validator_accounts
202            .get(pubkey)
203            .and_then(|opt| opt.clone())
204    }
205
206    /// Get all validator accounts from this cache (single layer).
207    ///
208    /// Note: This is a single-layer lookup. For merged view across all layers,
209    /// use `StakesHandle::get_all_validator_accounts`.
210    pub fn get_all_validator_accounts(&self) -> Vec<(Pubkey, ValidatorAccount)> {
211        let data = self.read();
212        data.validator_accounts
213            .iter()
214            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
215            .collect()
216    }
217
218    /// Check if a stake account exists in this cache (single layer).
219    pub fn contains_stake_account(&self, pubkey: &Pubkey) -> bool {
220        let data = self.read();
221        matches!(data.stake_accounts.get(pubkey), Some(Some(_)))
222    }
223
224    /// Check if a validator account exists in this cache (single layer).
225    pub fn contains_validator_account(&self, pubkey: &Pubkey) -> bool {
226        let data = self.read();
227        matches!(data.validator_accounts.get(pubkey), Some(Some(_)))
228    }
229
230    /// Insert or update a stake account.
231    ///
232    /// Also tracks the pubkey as modified for persistence.
233    pub fn insert_stake_account(&self, pubkey: Pubkey, account: StakeAccount) {
234        let mut data = self.write();
235        data.stake_accounts.insert(pubkey, Some(account));
236        data.modified_stake_pubkeys.insert(pubkey);
237    }
238
239    /// Insert or update a validator account.
240    ///
241    /// Also tracks the pubkey as modified for persistence.
242    pub fn insert_validator_account(&self, pubkey: Pubkey, account: ValidatorAccount) {
243        let mut data = self.write();
244        data.validator_accounts.insert(pubkey, Some(account));
245        data.modified_validator_pubkeys.insert(pubkey);
246    }
247
248    /// Insert a tombstone for a stake account (marks as deleted).
249    ///
250    /// Also tracks the pubkey as modified for persistence.
251    pub fn tombstone_stake_account(&self, pubkey: Pubkey) {
252        let mut data = self.write();
253        data.stake_accounts.insert(pubkey, None);
254        data.modified_stake_pubkeys.insert(pubkey);
255    }
256
257    /// Insert a tombstone for a validator account (marks as deleted).
258    ///
259    /// Also tracks the pubkey as modified for persistence.
260    pub fn tombstone_validator_account(&self, pubkey: Pubkey) {
261        let mut data = self.write();
262        data.validator_accounts.insert(pubkey, None);
263        data.modified_validator_pubkeys.insert(pubkey);
264    }
265
266    /// Get the epoch of this cache.
267    pub fn epoch(&self) -> Epoch {
268        self.read().epoch
269    }
270
271    /// Get the timestamp of this cache.
272    pub fn timestamp(&self) -> u64 {
273        self.read().timestamp
274    }
275
276    /// Set the epoch of this cache.
277    pub fn set_epoch(&self, epoch: Epoch) {
278        self.write().epoch = epoch;
279    }
280
281    /// Set the timestamp of this cache.
282    pub fn set_timestamp(&self, timestamp: u64) {
283        self.write().timestamp = timestamp;
284    }
285
286    /// Check an account and store it in the appropriate cache if it belongs to
287    /// StakeManager or ValidatorRegistry programs.
288    ///
289    /// - If the account has zero kelvins, it is evicted from the cache (tombstoned)
290    /// - If the account is owned by StakeManager, it is stored in stake_accounts
291    /// - If the account is owned by ValidatorRegistry, it is stored in validator_accounts
292    pub fn check_and_update(&self, pubkey: &Pubkey, account: &impl ReadableAccount) {
293        let owner = account.owner();
294
295        // Zero kelvin accounts should be marked as tombstones (None) in the delta
296        if account.kelvins() == 0 {
297            if rialo_stake_manager_interface::check_id(owner) {
298                // Insert tombstone (None) to mark deletion in this epoch's delta
299                self.tombstone_stake_account(*pubkey);
300            } else if rialo_validator_registry_interface::check_id(owner) {
301                // Insert tombstone (None) to mark deletion in this epoch's delta
302                self.tombstone_validator_account(*pubkey);
303            }
304        } else if rialo_stake_manager_interface::check_id(owner) {
305            // Handle StakeManager accounts
306            if let Ok(stake_info) = bincode::deserialize::<StakeInfo>(account.data()) {
307                self.insert_stake_account(
308                    *pubkey,
309                    StakeAccount {
310                        kelvins: account.kelvins(),
311                        data: stake_info,
312                    },
313                );
314            }
315        } else if rialo_validator_registry_interface::check_id(owner) {
316            // Handle ValidatorRegistry accounts
317            if let Ok(validator_info) = bincode::deserialize::<ValidatorInfo>(account.data()) {
318                self.insert_validator_account(
319                    *pubkey,
320                    ValidatorAccount {
321                        kelvins: account.kelvins(),
322                        data: validator_info,
323                    },
324                );
325            }
326        }
327    }
328}
329
330/// Data structure holding the cached stake and validator accounts.
331///
332/// Uses `HashMap<Pubkey, Option<T>>` to support the delta-based persistence model:
333/// - `Some(account)` = account was added or updated
334/// - `None` = account was deleted (tombstone)
335///
336/// In `baseline`, values are always `Some(...)` since it represents complete state.
337/// In `pending` and `frozen` deltas, `None` indicates deletion.
338#[derive(Debug, Default, Clone)]
339pub struct StakeCacheData {
340    /// Map of stake accounts by public key.
341    /// `None` value indicates a tombstone (account was deleted during this epoch).
342    pub stake_accounts: HashMap<Pubkey, Option<StakeAccount>>,
343    /// Map of validator accounts by public key.
344    /// `None` value indicates a tombstone (account was deleted during this epoch).
345    pub validator_accounts: HashMap<Pubkey, Option<ValidatorAccount>>,
346    /// The epoch counter when this snapshot was taken.
347    pub epoch: Epoch,
348    /// The block's Unix timestamp (in milliseconds) when this snapshot was taken.
349    /// This is set when FreezeStakes is called and represents the epoch boundary.
350    pub timestamp: u64,
351    /// Set of stake account pubkeys modified during the current block.
352    /// Used to track which accounts need to be persisted to the deltas CF.
353    /// This is cleared after each `finalize()` call.
354    pub modified_stake_pubkeys: HashSet<Pubkey>,
355    /// Set of validator account pubkeys modified during the current block.
356    /// Used to track which accounts need to be persisted to the deltas CF.
357    /// This is cleared after each `finalize()` call.
358    pub modified_validator_pubkeys: HashSet<Pubkey>,
359    /// Block timestamp when consensus adopted this epoch's stakes via Handover.
360    /// - `None` = not yet adopted (FreezeStakes guard will block the next freeze)
361    /// - `Some(0)` = genesis epoch (adopted at network start, rewards zeroed)
362    /// - `Some(ts)` = adopted at timestamp `ts` (Handover was processed)
363    pub consensus_adopted_at: Option<u64>,
364}
365
366impl StakeCacheData {
367    /// Drain the modified pubkey sets, returning the pubkeys and clearing the sets.
368    ///
369    /// This is called by `StateStore::finalize()` to get the list of accounts
370    /// that need to be persisted to the deltas CF. After this call, both
371    /// `modified_stake_pubkeys` and `modified_validator_pubkeys` will be empty.
372    ///
373    /// Returns a tuple of `(stake_pubkeys, validator_pubkeys)`.
374    pub fn drain_modified(&mut self) -> (HashSet<Pubkey>, HashSet<Pubkey>) {
375        let stake_pubkeys = std::mem::take(&mut self.modified_stake_pubkeys);
376        let validator_pubkeys = std::mem::take(&mut self.modified_validator_pubkeys);
377        (stake_pubkeys, validator_pubkeys)
378    }
379
380    /// Check if there are any modified accounts pending persistence.
381    pub fn has_modified(&self) -> bool {
382        !self.modified_stake_pubkeys.is_empty() || !self.modified_validator_pubkeys.is_empty()
383    }
384}
385
386/// A history of frozen stake cache snapshots across epochs.
387///
388/// This wraps `VecDeque<StakeCacheData>` in `Arc<RwLock<...>>` to allow thread-safe
389/// shared access. The Arc allows the same history to be shared between the Bank
390/// and StakesHandle.
391///
392/// This maintains a queue of stake snapshots, with the oldest at the front
393/// and the most recent at the back. The ValidatorRegistry builtin pushes
394/// new snapshots, and the Bank pops completed epochs after reward distribution.
395#[derive(Debug, Clone)]
396pub struct StakeHistory(Arc<RwLock<VecDeque<StakeCacheData>>>);
397
398impl Default for StakeHistory {
399    fn default() -> Self {
400        Self(Arc::new(RwLock::new(VecDeque::new())))
401    }
402}
403
404impl StakeHistory {
405    /// Create a new empty stake history.
406    pub fn new() -> Self {
407        Self::default()
408    }
409
410    /// Create a stake history with an initial entry.
411    pub fn with_entry(data: StakeCacheData) -> Self {
412        let mut deque = VecDeque::new();
413        deque.push_back(data);
414        Self(Arc::new(RwLock::new(deque)))
415    }
416
417    /// Create a stake history from an existing Arc (for sharing references).
418    pub fn from_arc(arc: Arc<RwLock<VecDeque<StakeCacheData>>>) -> Self {
419        Self(arc)
420    }
421
422    /// Get a clone of the inner Arc for sharing.
423    pub fn arc_clone(&self) -> Arc<RwLock<VecDeque<StakeCacheData>>> {
424        Arc::clone(&self.0)
425    }
426
427    /// Acquire a read lock on the inner data.
428    pub fn read(&self) -> std::sync::RwLockReadGuard<'_, VecDeque<StakeCacheData>> {
429        self.0.read().expect("Failed to acquire read lock")
430    }
431
432    /// Acquire a write lock on the inner data.
433    pub fn write_lock(&self) -> std::sync::RwLockWriteGuard<'_, VecDeque<StakeCacheData>> {
434        self.0.write().expect("Failed to acquire write lock")
435    }
436
437    /// Push a new snapshot to the back of the history.
438    pub fn push_back(&self, data: StakeCacheData) {
439        self.0
440            .write()
441            .expect("Failed to acquire lock")
442            .push_back(data);
443    }
444
445    /// Pop the oldest snapshot from the front of the history.
446    pub fn pop_front(&self) -> Option<StakeCacheData> {
447        self.0.write().expect("Failed to acquire lock").pop_front()
448    }
449
450    /// Get the number of snapshots in the history.
451    pub fn len(&self) -> usize {
452        self.0.read().expect("Failed to acquire lock").len()
453    }
454
455    /// Check if the history is empty.
456    pub fn is_empty(&self) -> bool {
457        self.0.read().expect("Failed to acquire lock").is_empty()
458    }
459
460    /// Get a clone of the oldest snapshot (front).
461    pub fn front(&self) -> Option<StakeCacheData> {
462        self.0
463            .read()
464            .expect("Failed to acquire lock")
465            .front()
466            .cloned()
467    }
468
469    /// Get a clone of the newest snapshot (back).
470    ///
471    /// This returns the CURRENT epoch's frozen stake data. In normal operation,
472    /// this is never `None` because Bank initialization guarantees at least one
473    /// entry exists after genesis/register_validators.
474    ///
475    /// Use this for lookups that need the current epoch's effective stake state
476    /// (as opposed to `StakesHandle::pending` which is the next epoch being accumulated).
477    pub fn back(&self) -> Option<StakeCacheData> {
478        self.0
479            .read()
480            .expect("Failed to acquire lock")
481            .back()
482            .cloned()
483    }
484
485    /// Iterate over all snapshots from oldest to newest, returning cloned data.
486    ///
487    /// Note: This clones all entries. For large histories, consider accessing
488    /// specific entries via `front()` or `back()` instead.
489    pub fn iter_cloned(&self) -> Vec<StakeCacheData> {
490        self.0
491            .read()
492            .expect("Failed to acquire lock")
493            .iter()
494            .cloned()
495            .collect()
496    }
497}
498
499/// Represents a stake account with its data.
500#[derive(Debug, Clone)]
501pub struct StakeAccount {
502    /// The kelvins balance of the stake account.
503    pub kelvins: u64,
504    /// The deserialized stake info.
505    pub data: StakeInfo,
506}
507
508/// Represents a validator account with its data.
509#[derive(Debug, Clone)]
510pub struct ValidatorAccount {
511    /// The kelvins balance of the validator account.
512    pub kelvins: u64,
513    /// The deserialized validator info.
514    pub data: ValidatorInfo,
515}
516
517/// Handle for builtin programs to access stake cache data and freeze stakes.
518///
519/// This handle provides:
520/// - Read/write access to the pending (next epoch) stake cache data
521/// - Layered lookup across baseline, frozen, and pending
522/// - The ability to freeze the pending stakes into frozen via `freeze_stakes()`
523/// - Callback to check if EpochRewards exists for a given epoch
524///
525/// # Architecture: Baseline + Deltas
526///
527/// The stake cache uses a layered architecture:
528/// - **baseline**: Complete historical state (empty at genesis, populated during EpochRewards activation)
529/// - **frozen**: VecDeque of per-epoch deltas awaiting reward distribution (FIFO order)
530/// - **pending**: Current epoch's changes being accumulated
531///
532/// Lookups search: pending → frozen (newest to oldest) → baseline
533///
534/// # Epoch Semantics
535///
536/// **Important:** The `pending` field contains data for the NEXT epoch (i.e., changes being
537/// accumulated that will take effect after FreezeStakes). To get the CURRENT epoch's frozen
538/// data for lookups, use `frozen.back()` instead.
539///
540/// The handle is cached at block level for performance. Since the handle uses shared
541/// `Arc<RwLock<...>>` references, mutations to pending are immediately visible without
542/// needing to recreate the handle.
543///
544/// # Thread Safety
545///
546/// `StakeCache` and `StakeHistory` wrap their data in `Arc<RwLock<...>>` internally,
547/// allowing safe concurrent access from builtin programs during transaction execution.
548/// Mutations to `pending` are immediately visible to the owning Bank since they share
549/// the same Arc.
550///
551/// # Field Access
552///
553/// The `baseline`, `pending`, and `frozen` fields are private to enforce proper layered lookups.
554/// Use the provided methods for queries:
555/// - `get_stake_account()` - layered lookup for a single stake account
556/// - `get_validator_account()` - layered lookup for a single validator account
557/// - `get_all_validator_accounts()` - merged view of all validators
558/// - `freeze_stakes()` - freeze pending stakes
559/// - `epoch_rewards_exists()` - check if EpochRewards account exists for an epoch
560///
561/// Direct field access is only available via `#[cfg(test)]` accessors for unit tests.
562pub struct StakesHandle {
563    /// Complete state at historical epoch boundary (for fallback lookups).
564    ///
565    /// At genesis, this is empty. After EpochRewards activation, it contains all accounts
566    /// that existed before the oldest epoch still awaiting reward distribution.
567    /// Values are always `Some(...)` in the baseline (no tombstones).
568    baseline: StakeCache,
569
570    /// Stake cache data for the NEXT epoch (pending/accumulating changes).
571    ///
572    /// This is a mutable working copy that accumulates stake and validator account
573    /// modifications throughout the epoch. These changes will become effective after
574    /// the next FreezeStakes call. For current epoch lookups (the frozen effective
575    /// state), use `frozen.back()` instead.
576    ///
577    /// The `StakeCache` wrapper contains `Arc<RwLock<...>>` internally, allowing
578    /// builtin programs to mutate the pending stake data during transaction execution,
579    /// with changes visible to the Bank.
580    pending: StakeCache,
581
582    /// Frozen snapshots for epochs awaiting reward distribution (FIFO order).
583    ///
584    /// Each entry contains ONLY the accounts that changed during that epoch
585    /// (delta, not full state). `Some(account)` = added/updated, `None` = deleted.
586    /// The oldest entry is at the front, the newest at the back.
587    ///
588    /// The `StakeHistory` wrapper contains `Arc<RwLock<...>>` internally.
589    frozen: StakeHistory,
590
591    /// Data for epoch rewards initialization (epoch number and total rewards).
592    /// Set by `request_epoch_rewards_init()`, consumed by `take_epoch_rewards_init_request()`.
593    epoch_rewards_init: Arc<RwLock<Option<EpochRewardsInitRequest>>>,
594
595    /// Signal that the `FreezeStakes` instruction was executed.
596    ///
597    /// The actual pending → frozen swap is deferred to
598    /// `Bank::finalize_impl()`, which consumes the signal and
599    /// performs the in-memory swap by calling `freeze_stakes()`.
600    ///
601    /// Before consumption, `apply_pending_validator_changes_if_needed()`
602    /// observes the signal to apply pending validator changes (e.g.,
603    /// `new_commission_rate` → `commission_rate`) at the epoch boundary.
604    epoch_stakes_frozen: Arc<AtomicBool>,
605
606    /// Signal requesting that the next clock-subscription-triggered
607    /// `FreezeStakes(force=false)` in the current commit skip its time guard
608    /// and suppress the `distribute_rewards_ready` emission.
609    ///
610    /// Set by a manual `FreezeStakes(force=true)` user transaction as its
611    /// only side effect (see `processor.rs`); consumed atomically by the
612    /// triggered `FreezeStakes(force=false)` via `take_force_next_auto_freeze()`
613    /// later in the same commit, which then bypasses `should_skip_auto_freeze`
614    /// and omits the `distribute_rewards_ready` event.
615    ///
616    /// `FreezeStakes(force=true)` is today only submitted by the integration
617    /// test harness, but it is a governance transaction that a future
618    /// `TOKENOMICS_GOVERNANCE_AUTHORITY` (e.g. DAO-voting / multisig
619    /// PDA) may legitimately submit to force an out-of-schedule freeze.
620    force_next_auto_freeze: Arc<AtomicBool>,
621
622    /// Callback to check if an EpochRewards account exists for a given epoch.
623    /// Provided by Bank with access to StateStore. Used by DistributeRewards
624    /// to find the first completed frozen epoch without an EpochRewards account.
625    /// Set at construction time, immutable afterwards.
626    epoch_rewards_exists_fn: Arc<dyn Fn(u64) -> bool + Send + Sync>,
627
628    /// Signal carrying the block timestamp when a Handover admin transaction was
629    /// detected. Set by `signal_handover()` in the ExecutionEngine, consumed by
630    /// `take_handover()` in `Bank::finalize_impl()`.
631    handover_ts: Arc<RwLock<Option<u64>>>,
632}
633
634/// Request data for epoch rewards initialization.
635/// Used to pass information from DistributeRewards instruction to Bank.
636#[derive(Debug, Clone)]
637pub struct EpochRewardsInitRequest {
638    /// The epoch for which rewards are being distributed.
639    pub epoch: Epoch,
640    /// Per-epoch inflation budget, computed by `DistributeRewards`.
641    pub total_rewards: u64,
642    /// Total eligible stake (in kelvins) at the frozen snapshot of `epoch`.
643    pub total_eligible_stake: u64,
644    /// Per-validator scores in basis points, ordered by validator pubkey
645    /// ascending across the frozen epoch's validator set.
646    pub validator_scores: Vec<u32>,
647}
648
649impl std::fmt::Debug for StakesHandle {
650    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651        f.debug_struct("StakesHandle")
652            .field("baseline", &self.baseline)
653            .field("pending", &self.pending)
654            .field("frozen", &self.frozen)
655            .field("epoch_rewards_init", &self.epoch_rewards_init)
656            .field("epoch_stakes_frozen", &self.epoch_stakes_frozen)
657            .field("force_next_auto_freeze", &self.force_next_auto_freeze)
658            .field("epoch_rewards_exists_fn", &"<callback>")
659            .finish()
660    }
661}
662
663impl Clone for StakesHandle {
664    fn clone(&self) -> Self {
665        Self {
666            baseline: self.baseline.clone(),
667            pending: self.pending.clone(),
668            frozen: self.frozen.clone(),
669            epoch_rewards_init: self.epoch_rewards_init.clone(),
670            epoch_stakes_frozen: Arc::clone(&self.epoch_stakes_frozen),
671            force_next_auto_freeze: Arc::clone(&self.force_next_auto_freeze),
672            epoch_rewards_exists_fn: Arc::clone(&self.epoch_rewards_exists_fn),
673            handover_ts: self.handover_ts.clone(),
674        }
675    }
676}
677
678impl Default for StakesHandle {
679    fn default() -> Self {
680        Self {
681            baseline: StakeCache::default(),
682            pending: StakeCache::default(),
683            frozen: StakeHistory::default(),
684            epoch_rewards_init: Arc::new(RwLock::new(None)),
685            epoch_stakes_frozen: Arc::new(AtomicBool::new(false)),
686            force_next_auto_freeze: Arc::new(AtomicBool::new(false)),
687            epoch_rewards_exists_fn: Arc::new(|_| false),
688            handover_ts: Arc::new(RwLock::new(None)),
689        }
690    }
691}
692
693impl StakesHandle {
694    /// Create a new stakes handle with shared references.
695    ///
696    /// This shares the same `Arc<RwLock<...>>` with the Bank, so mutations
697    /// to `pending` by builtin programs are immediately visible to the Bank.
698    ///
699    /// The signaling Arcs (`epoch_rewards_init`, `epoch_stakes_frozen`)
700    /// are created internally with default values. This simplifies the API since callers
701    /// don't need to manage these internal signaling mechanisms.
702    ///
703    /// # Arguments
704    /// * `baseline` - The baseline stake cache
705    /// * `pending` - The pending stake cache for the next epoch
706    /// * `frozen` - The frozen stake history
707    /// * `epoch_rewards_exists_fn` - Callback to check if an EpochRewards account exists
708    pub fn new_shared(
709        baseline: StakeCache,
710        pending: StakeCache,
711        frozen: StakeHistory,
712        epoch_rewards_exists_fn: Arc<dyn Fn(u64) -> bool + Send + Sync>,
713    ) -> Self {
714        Self {
715            baseline,
716            pending,
717            frozen,
718            epoch_rewards_init: Arc::new(RwLock::new(None)),
719            epoch_stakes_frozen: Arc::new(AtomicBool::new(false)),
720            force_next_auto_freeze: Arc::new(AtomicBool::new(false)),
721            epoch_rewards_exists_fn,
722            handover_ts: Arc::new(RwLock::new(None)),
723        }
724    }
725
726    /// Check if an EpochRewards account exists for the given epoch.
727    ///
728    /// Uses the callback provided at construction time to query the StateStore.
729    /// This allows DistributeRewards to find the first completed frozen epoch
730    /// that doesn't yet have an EpochRewards account.
731    pub fn epoch_rewards_exists(&self, epoch: u64) -> bool {
732        (self.epoch_rewards_exists_fn)(epoch)
733    }
734
735    /// Signal that epoch stakes have been frozen (FreezeStakes was called).
736    ///
737    /// This sets the `epoch_stakes_frozen` flag to true to signal that
738    /// `apply_pending_validator_changes_if_needed()` should be called by the Bank.
739    pub fn set_epoch_stakes_frozen(&self) {
740        self.epoch_stakes_frozen.store(true, Ordering::Release);
741    }
742
743    /// Atomically take the epoch_stakes_frozen signal.
744    ///
745    /// This atomically reads and clears the flag, returning `true` if it was set.
746    /// Used by `finalize_impl()` to consume the signal and perform the deferred
747    /// pending → frozen swap.
748    ///
749    /// Returns `true` if FreezeStakes was called and the signal hadn't been consumed yet.
750    pub fn take_epoch_stakes_frozen(&self) -> bool {
751        self.epoch_stakes_frozen.swap(false, Ordering::AcqRel)
752    }
753
754    /// Check if FreezeStakes was signaled this block, without consuming the flag.
755    ///
756    /// Used by `apply_pending_validator_changes_if_needed()` to detect the epoch
757    /// boundary while leaving the flag set for `finalize_impl()` to consume.
758    pub fn is_epoch_stakes_frozen(&self) -> bool {
759        self.epoch_stakes_frozen.load(Ordering::Acquire)
760    }
761
762    /// Set the `force_next_auto_freeze` signal to `true`.
763    ///
764    /// See the field documentation on `StakesHandle::force_next_auto_freeze`
765    /// for the end-to-end flow.
766    pub fn set_force_next_auto_freeze(&self) {
767        self.force_next_auto_freeze.store(true, Ordering::Release);
768    }
769
770    /// Atomically read and clear the `force_next_auto_freeze` signal.
771    ///
772    /// Returns `true` if a manual `FreezeStakes(force=true)` earlier in this
773    /// commit requested the guard bypass. The atomic `swap` ensures back-to-back
774    /// clock subscriptions cannot double-bypass.
775    ///
776    /// See the field documentation on `StakesHandle::force_next_auto_freeze`
777    /// for the end-to-end flow.
778    pub fn take_force_next_auto_freeze(&self) -> bool {
779        self.force_next_auto_freeze.swap(false, Ordering::AcqRel)
780    }
781
782    // ========== Layered Lookups from Pending ==========
783    // These methods include pending changes (next epoch) in the lookup.
784
785    /// Get a stake account starting from pending (next epoch state).
786    ///
787    /// Searches: pending → frozen (newest to oldest) → baseline
788    ///
789    /// Returns `Some(account)` if found, `None` if the account doesn't exist
790    /// (either never created or was deleted via tombstone).
791    pub fn get_stake_account_from_pending(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
792        // 1. Check pending (next epoch)
793        {
794            let pending_data = self.pending.read();
795            if let Some(value) = pending_data.stake_accounts.get(pubkey) {
796                return value.clone(); // Some(account) or None (tombstone)
797            }
798        }
799
800        // 2. Check frozen epochs in reverse order (newest to oldest)
801        {
802            let frozen_data = self.frozen.read();
803            for frozen_entry in frozen_data.iter().rev() {
804                if let Some(value) = frozen_entry.stake_accounts.get(pubkey) {
805                    return value.clone();
806                }
807            }
808        }
809
810        // 3. Check baseline
811        {
812            let baseline_data = self.baseline.read();
813            baseline_data
814                .stake_accounts
815                .get(pubkey)
816                .and_then(|v| v.clone())
817        }
818    }
819
820    /// Get a validator account starting from pending (next epoch state).
821    ///
822    /// Searches: pending → frozen (newest to oldest) → baseline
823    ///
824    /// Returns `Some(account)` if found, `None` if the account doesn't exist
825    /// (either never created or was deleted via tombstone).
826    pub fn get_validator_account_from_pending(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
827        // 1. Check pending (next epoch)
828        {
829            let pending_data = self.pending.read();
830            if let Some(value) = pending_data.validator_accounts.get(pubkey) {
831                return value.clone(); // Some(account) or None (tombstone)
832            }
833        }
834
835        // 2. Check frozen epochs in reverse order (newest to oldest)
836        {
837            let frozen_data = self.frozen.read();
838            for frozen_entry in frozen_data.iter().rev() {
839                if let Some(value) = frozen_entry.validator_accounts.get(pubkey) {
840                    return value.clone();
841                }
842            }
843        }
844
845        // 3. Check baseline
846        {
847            let baseline_data = self.baseline.read();
848            baseline_data
849                .validator_accounts
850                .get(pubkey)
851                .and_then(|v| v.clone())
852        }
853    }
854
855    /// Get all validator accounts starting from pending (next epoch state).
856    ///
857    /// Returns a vector of `(pubkey, account)` pairs for all validators, sorted by pubkey.
858    /// Includes pending changes (next epoch).
859    /// Note: This is O(baseline_size + total_deltas).
860    pub fn get_all_validator_accounts_from_pending(&self) -> Vec<(Pubkey, ValidatorAccount)> {
861        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
862
863        // 1. Start with baseline
864        {
865            let baseline_data = self.baseline.read();
866            for (pubkey, value) in baseline_data.validator_accounts.iter() {
867                result.insert(*pubkey, value.clone());
868            }
869        }
870
871        // 2. Apply frozen deltas in order (oldest to newest)
872        {
873            let frozen_data = self.frozen.read();
874            for frozen_entry in frozen_data.iter() {
875                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
876                    result.insert(*pubkey, value.clone());
877                }
878            }
879        }
880
881        // 3. Apply pending deltas
882        {
883            let pending_data = self.pending.read();
884            for (pubkey, value) in pending_data.validator_accounts.iter() {
885                result.insert(*pubkey, value.clone());
886            }
887        }
888
889        // 4. Filter out tombstones and collect
890        let mut sorted: Vec<_> = result
891            .into_iter()
892            .filter_map(|(k, v)| v.map(|account| (k, account)))
893            .collect();
894
895        // Sort by pubkey for deterministic ordering
896        sorted.sort_by_key(|(pubkey, _)| *pubkey);
897        sorted
898    }
899
900    // ========== Layered Lookups from Last Frozen ==========
901    // These methods represent the current epoch's effective state (skip pending).
902
903    /// Get a stake account starting from the last frozen epoch (current epoch state).
904    ///
905    /// Searches: frozen (newest to oldest) → baseline
906    /// Skips pending (next epoch changes).
907    ///
908    /// Returns `Some(account)` if found, `None` if the account doesn't exist
909    /// (either never created or was deleted via tombstone).
910    pub fn get_stake_account_from_last_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
911        // 1. Check frozen epochs in reverse order (newest to oldest)
912        {
913            let frozen_data = self.frozen.read();
914            for frozen_entry in frozen_data.iter().rev() {
915                if let Some(value) = frozen_entry.stake_accounts.get(pubkey) {
916                    return value.clone();
917                }
918            }
919        }
920
921        // 2. Check baseline
922        {
923            let baseline_data = self.baseline.read();
924            baseline_data
925                .stake_accounts
926                .get(pubkey)
927                .and_then(|v| v.clone())
928        }
929    }
930
931    /// Get a validator account starting from the last frozen epoch (current epoch state).
932    ///
933    /// Searches: frozen (newest to oldest) → baseline
934    /// Skips pending (next epoch changes).
935    ///
936    /// Returns `Some(account)` if found, `None` if the account doesn't exist
937    /// (either never created or was deleted via tombstone).
938    pub fn get_validator_account_from_last_frozen(
939        &self,
940        pubkey: &Pubkey,
941    ) -> Option<ValidatorAccount> {
942        // 1. Check frozen epochs in reverse order (newest to oldest)
943        {
944            let frozen_data = self.frozen.read();
945            for frozen_entry in frozen_data.iter().rev() {
946                if let Some(value) = frozen_entry.validator_accounts.get(pubkey) {
947                    return value.clone();
948                }
949            }
950        }
951
952        // 2. Check baseline
953        {
954            let baseline_data = self.baseline.read();
955            baseline_data
956                .validator_accounts
957                .get(pubkey)
958                .and_then(|v| v.clone())
959        }
960    }
961
962    /// Get all validator accounts from the last frozen epoch (current epoch state).
963    ///
964    /// Returns a vector of `(pubkey, account)` pairs for all validators, sorted by pubkey.
965    /// Skips pending (next epoch changes).
966    /// Note: This is O(baseline_size + total_frozen_deltas).
967    pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
968        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
969
970        // 1. Start with baseline
971        {
972            let baseline_data = self.baseline.read();
973            for (pubkey, value) in baseline_data.validator_accounts.iter() {
974                result.insert(*pubkey, value.clone());
975            }
976        }
977
978        // 2. Apply all frozen deltas in order (oldest to newest)
979        {
980            let frozen_data = self.frozen.read();
981            for frozen_entry in frozen_data.iter() {
982                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
983                    result.insert(*pubkey, value.clone());
984                }
985            }
986        }
987
988        // 3. Filter out tombstones and collect (skip pending)
989        let mut sorted: Vec<_> = result
990            .into_iter()
991            .filter_map(|(k, v)| v.map(|account| (k, account)))
992            .collect();
993
994        // Sort by pubkey for deterministic ordering
995        sorted.sort_by_key(|(pubkey, _)| *pubkey);
996        sorted
997    }
998
999    /// Find a validator by their authority key from the last frozen epoch (current epoch state).
1000    ///
1001    /// Performs the same layered merge as `get_all_validator_accounts_from_last_frozen()`
1002    /// (baseline + frozen deltas, skips pending) but scans for a matching `authority_key`
1003    /// instead of collecting all validators into a sorted Vec.
1004    ///
1005    /// Returns the `ValidatorInfo` if found, `None` otherwise.
1006    pub fn find_validator_by_authority_key_from_last_frozen(
1007        &self,
1008        authority_key: &[u8],
1009    ) -> Option<ValidatorInfo> {
1010        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
1011
1012        // 1. Start with baseline
1013        {
1014            let baseline_data = self.baseline.read();
1015            for (pubkey, value) in baseline_data.validator_accounts.iter() {
1016                result.insert(*pubkey, value.clone());
1017            }
1018        }
1019
1020        // 2. Apply all frozen deltas in order (oldest to newest)
1021        {
1022            let frozen_data = self.frozen.read();
1023            for frozen_entry in frozen_data.iter() {
1024                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
1025                    result.insert(*pubkey, value.clone());
1026                }
1027            }
1028        }
1029
1030        // 3. Scan for matching authority_key (skip tombstones, no sort needed)
1031        result
1032            .into_values()
1033            .flatten()
1034            .find(|account| account.data.authority_key == authority_key)
1035            .map(|account| account.data)
1036    }
1037
1038    // ========== Layered Lookups from First Frozen ==========
1039    // These methods represent the oldest pending rewards epoch state.
1040
1041    /// Get a stake account starting from the first frozen epoch (oldest pending rewards).
1042    ///
1043    /// Searches: frozen.front() → baseline only
1044    /// Skips all newer frozen epochs and pending.
1045    ///
1046    /// Returns `Some(account)` if found, `None` if the account doesn't exist
1047    /// (either never created or was deleted via tombstone).
1048    pub fn get_stake_account_from_first_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
1049        // 1. Check first frozen epoch only
1050        {
1051            let frozen_data = self.frozen.read();
1052            if let Some(first_frozen) = frozen_data.front() {
1053                if let Some(value) = first_frozen.stake_accounts.get(pubkey) {
1054                    return value.clone();
1055                }
1056            }
1057        }
1058
1059        // 2. Check baseline
1060        {
1061            let baseline_data = self.baseline.read();
1062            baseline_data
1063                .stake_accounts
1064                .get(pubkey)
1065                .and_then(|v| v.clone())
1066        }
1067    }
1068
1069    /// Get all stake accounts starting from pending (next epoch state).
1070    ///
1071    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts, sorted by pubkey.
1072    /// Includes pending changes (next epoch).
1073    /// Note: This is O(baseline_size + total_deltas).
1074    ///
1075    /// This method is used for operations that need to check all stake accounts
1076    /// including the most recent changes (e.g., checking validator references during Withdraw).
1077    pub fn get_all_stake_accounts_from_pending(&self) -> Vec<(Pubkey, StakeAccount)> {
1078        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
1079
1080        // 1. Start with baseline
1081        {
1082            let baseline_data = self.baseline.read();
1083            for (pubkey, value) in baseline_data.stake_accounts.iter() {
1084                result.insert(*pubkey, value.clone());
1085            }
1086        }
1087
1088        // 2. Apply frozen deltas in order (oldest to newest)
1089        {
1090            let frozen_data = self.frozen.read();
1091            for frozen_entry in frozen_data.iter() {
1092                for (pubkey, value) in frozen_entry.stake_accounts.iter() {
1093                    result.insert(*pubkey, value.clone());
1094                }
1095            }
1096        }
1097
1098        // 3. Apply pending deltas
1099        {
1100            let pending_data = self.pending.read();
1101            for (pubkey, value) in pending_data.stake_accounts.iter() {
1102                result.insert(*pubkey, value.clone());
1103            }
1104        }
1105
1106        // 4. Filter out tombstones and collect
1107        let mut sorted: Vec<_> = result
1108            .into_iter()
1109            .filter_map(|(k, v)| v.map(|account| (k, account)))
1110            .collect();
1111
1112        // Sort by pubkey for deterministic ordering
1113        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1114        sorted
1115    }
1116
1117    /// Get all stake accounts from the first frozen epoch (oldest pending rewards).
1118    ///
1119    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts, sorted by pubkey.
1120    /// Skips all newer frozen epochs and pending.
1121    ///
1122    /// This method is used by reward calculation to iterate over all stake accounts
1123    /// that were active at the time rewards were frozen (baseline + first frozen delta).
1124    pub fn get_all_stake_accounts_from_first_frozen(&self) -> Vec<(Pubkey, StakeAccount)> {
1125        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
1126
1127        // 1. Start with baseline
1128        {
1129            let baseline_data = self.baseline.read();
1130            for (pubkey, value) in baseline_data.stake_accounts.iter() {
1131                result.insert(*pubkey, value.clone());
1132            }
1133        }
1134
1135        // 2. Apply only the first frozen delta
1136        {
1137            let frozen_data = self.frozen.read();
1138            if let Some(first_frozen) = frozen_data.front() {
1139                for (pubkey, value) in first_frozen.stake_accounts.iter() {
1140                    result.insert(*pubkey, value.clone());
1141                }
1142            }
1143        }
1144
1145        // 3. Filter out tombstones and collect
1146        let mut sorted: Vec<_> = result
1147            .into_iter()
1148            .filter_map(|(k, v)| v.map(|account| (k, account)))
1149            .collect();
1150
1151        // Sort by pubkey for deterministic ordering
1152        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1153        sorted
1154    }
1155
1156    /// Get all stake accounts from baseline + frozen deltas up to (and including)
1157    /// the specified epoch.
1158    ///
1159    /// Lookups: baseline + frozen deltas where `delta.epoch <= target_epoch`
1160    pub fn get_all_stake_accounts_from_frozen_epoch(
1161        &self,
1162        target_epoch: Epoch,
1163    ) -> Vec<(Pubkey, StakeAccount)> {
1164        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
1165
1166        // 1. Start with baseline
1167        {
1168            let baseline_data = self.baseline.read();
1169            for (pubkey, value) in baseline_data.stake_accounts.iter() {
1170                result.insert(*pubkey, value.clone());
1171            }
1172        }
1173
1174        // 2. Apply frozen deltas up to and including target_epoch
1175        {
1176            let frozen_data = self.frozen.read();
1177            for frozen_entry in frozen_data.iter() {
1178                if frozen_entry.epoch > target_epoch {
1179                    break; // Frozen is ordered oldest-to-newest, stop at target
1180                }
1181                for (pubkey, value) in frozen_entry.stake_accounts.iter() {
1182                    result.insert(*pubkey, value.clone());
1183                }
1184            }
1185        }
1186
1187        // 3. Filter out tombstones and collect
1188        let mut sorted: Vec<_> = result
1189            .into_iter()
1190            .filter_map(|(k, v)| v.map(|account| (k, account)))
1191            .collect();
1192
1193        // Sort by pubkey for deterministic ordering
1194        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1195        sorted
1196    }
1197
1198    /// Get all validator accounts from baseline + frozen deltas up to (and
1199    /// including) the specified epoch. Sorted by pubkey ascending.
1200    ///
1201    /// Lookups: baseline + frozen deltas where `delta.epoch <= target_epoch`
1202    pub fn get_all_validator_accounts_from_frozen_epoch(
1203        &self,
1204        target_epoch: Epoch,
1205    ) -> Vec<(Pubkey, ValidatorAccount)> {
1206        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
1207
1208        // 1. Start with baseline
1209        {
1210            let baseline_data = self.baseline.read();
1211            for (pubkey, value) in baseline_data.validator_accounts.iter() {
1212                result.insert(*pubkey, value.clone());
1213            }
1214        }
1215
1216        // 2. Apply frozen deltas up to and including target_epoch
1217        {
1218            let frozen_data = self.frozen.read();
1219            for frozen_entry in frozen_data.iter() {
1220                if frozen_entry.epoch > target_epoch {
1221                    break; // Frozen is ordered oldest-to-newest, stop at target
1222                }
1223                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
1224                    result.insert(*pubkey, value.clone());
1225                }
1226            }
1227        }
1228
1229        // 3. Filter out tombstones and collect
1230        let mut sorted: Vec<_> = result
1231            .into_iter()
1232            .filter_map(|(k, v)| v.map(|account| (k, account)))
1233            .collect();
1234
1235        // Sort by pubkey for deterministic ordering
1236        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1237        sorted
1238    }
1239
1240    // ========== Baseline-Only Lookups ==========
1241    // These methods return data from the baseline only (after merge has happened).
1242    // Used by the new reward calculation model where merge happens at activation.
1243
1244    /// Get all stake accounts from the baseline only.
1245    ///
1246    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts in baseline,
1247    /// sorted by pubkey. Does NOT include frozen or pending data.
1248    ///
1249    /// This is used after the frozen.front() has been merged into baseline,
1250    /// for baseline-based reward calculation. The baseline contains the complete
1251    /// state of the epoch being rewarded after merge.
1252    pub fn get_all_stake_accounts_from_baseline(&self) -> Vec<(Pubkey, StakeAccount)> {
1253        let baseline_data = self.baseline.read();
1254        let mut sorted: Vec<_> = baseline_data
1255            .stake_accounts
1256            .iter()
1257            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
1258            .collect();
1259
1260        // Sort by pubkey for deterministic ordering
1261        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1262        sorted
1263    }
1264
1265    /// Get all validator accounts from the baseline only.
1266    ///
1267    /// Returns a vector of `(pubkey, account)` pairs for all validators in baseline,
1268    /// sorted by pubkey. Does NOT include frozen or pending data.
1269    ///
1270    /// This is used after the frozen.front() has been merged into baseline,
1271    /// for baseline-based reward calculation.
1272    pub fn get_all_validator_accounts_from_baseline(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1273        let baseline_data = self.baseline.read();
1274        let mut sorted: Vec<_> = baseline_data
1275            .validator_accounts
1276            .iter()
1277            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
1278            .collect();
1279
1280        // Sort by pubkey for deterministic ordering
1281        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1282        sorted
1283    }
1284
1285    /// Freeze the pending stake cache data.
1286    ///
1287    /// This performs an O(1) swap of the pending stake cache data using `std::mem::take()`
1288    /// and pushes it to the back of the frozen queue. It is called by `Bank::finalize_impl()`
1289    /// once `take_epoch_stakes_frozen()` returns `true` (the deferred swap signaled by the
1290    /// `FreezeStakes` builtin), and by `Bank::new_with_paths()` for the eager genesis freeze.
1291    ///
1292    /// **Note:** Since the handle uses shared `Arc<RwLock<...>>` references, the frozen
1293    /// data and updated pending epoch are immediately visible to all handle instances.
1294    ///
1295    /// To access the frozen validator data after calling this method, use
1296    /// `get_all_validator_accounts_from_last_frozen()`.
1297    pub fn freeze_stakes(&self) {
1298        // Atomically swap pending data with empty and initialize new pending.
1299        // Using a single lock scope eliminates any race condition window.
1300        let frozen_data = {
1301            let mut pending_guard = self.pending.write();
1302            let frozen_data = std::mem::take(&mut *pending_guard);
1303            // Initialize new pending's epoch and timestamp for next epoch.
1304            // This ensures any stake changes after FreezeStakes within the same
1305            // block have the correct epoch/timestamp (not the Default values of 0).
1306            pending_guard.epoch = frozen_data.epoch + 1;
1307            pending_guard.timestamp = frozen_data.timestamp;
1308            frozen_data
1309        };
1310
1311        // Push frozen data to history.
1312        self.frozen.push_back(frozen_data);
1313    }
1314
1315    // ========== Epoch and Timestamp Accessors ==========
1316
1317    /// Get the epoch of the pending (next) stake cache.
1318    pub fn pending_epoch(&self) -> Epoch {
1319        self.pending.epoch()
1320    }
1321
1322    /// Set the epoch of the pending stake cache.
1323    pub fn set_pending_epoch(&self, epoch: Epoch) {
1324        self.pending.set_epoch(epoch);
1325    }
1326
1327    /// Set the timestamp of the pending stake cache.
1328    pub fn set_pending_timestamp(&self, timestamp: u64) {
1329        self.pending.set_timestamp(timestamp);
1330    }
1331
1332    /// Get the timestamp of the last frozen epoch (current epoch's effective state).
1333    ///
1334    /// Returns `None` if no frozen snapshots exist yet.
1335    pub fn last_frozen_timestamp(&self) -> Option<u64> {
1336        self.frozen.read().back().map(|data| data.timestamp)
1337    }
1338
1339    /// Get the epoch number of the last frozen snapshot (current epoch).
1340    /// Returns `None` if no frozen snapshots exist yet.
1341    pub fn last_frozen_epoch(&self) -> Option<Epoch> {
1342        self.frozen.read().back().map(|data| data.epoch)
1343    }
1344
1345    /// Get the timestamp of the pending stake cache.
1346    pub fn pending_timestamp(&self) -> u64 {
1347        self.pending.read().timestamp
1348    }
1349
1350    /// Push a new frozen snapshot to the history.
1351    pub fn push_frozen(&self, data: StakeCacheData) {
1352        self.frozen.push_back(data);
1353    }
1354
1355    /// Get the number of frozen snapshots in the history.
1356    pub fn frozen_len(&self) -> usize {
1357        self.frozen.len()
1358    }
1359
1360    /// Get the epoch of the oldest frozen snapshot (front of the queue).
1361    ///
1362    /// Returns `None` if no frozen snapshots exist.
1363    pub fn front_frozen_epoch(&self) -> Option<Epoch> {
1364        self.frozen.front().map(|data| data.epoch)
1365    }
1366
1367    /// Return `(timestamp, consensus_adopted_at)` of the frozen entry whose
1368    /// epoch matches `epoch`.
1369    ///
1370    /// Returns `None` if no frozen entry has that epoch. The inner
1371    /// `consensus_adopted_at` follows the field's own semantics: `None` =
1372    /// not yet adopted by consensus, `Some(0)` = genesis epoch (rewards
1373    /// zeroed), `Some(ts)` = adopted at timestamp `ts`.
1374    pub fn get_frozen_epoch_meta(&self, epoch: Epoch) -> Option<(u64, Option<u64>)> {
1375        self.frozen
1376            .read()
1377            .iter()
1378            .find(|d| d.epoch == epoch)
1379            .map(|d| (d.timestamp, d.consensus_adopted_at))
1380    }
1381
1382    /// Timestamp of the baseline snapshot.
1383    pub fn baseline_timestamp(&self) -> u64 {
1384        self.baseline.timestamp()
1385    }
1386
1387    // ========== Epoch Rewards Signaling ==========
1388
1389    /// Request epoch rewards initialization.
1390    ///
1391    /// This is called by the DistributeRewards instruction to signal that the Bank
1392    /// should create an EpochRewards account. The Bank checks for the request after
1393    /// transaction execution via `take_epoch_rewards_init_request()`.
1394    ///
1395    /// # Arguments
1396    /// * `epoch` - The epoch for which rewards are being distributed
1397    /// * `total_rewards` - Per-epoch inflation budget computed by `DistributeRewards`
1398    /// * `total_eligible_stake` - Total eligible stake (in kelvins) at the frozen
1399    ///   snapshot of `epoch`
1400    /// * `validator_scores` - Per-validator scores in basis points, ordered by
1401    ///   validator pubkey ascending across the frozen epoch's validator set
1402    pub fn request_epoch_rewards_init(
1403        &self,
1404        epoch: Epoch,
1405        total_rewards: u64,
1406        total_eligible_stake: u64,
1407        validator_scores: Vec<u32>,
1408    ) {
1409        // Store the request data - the presence of Some indicates a request is pending
1410        *self
1411            .epoch_rewards_init
1412            .write()
1413            .expect("Failed to acquire lock") = Some(EpochRewardsInitRequest {
1414            epoch,
1415            total_rewards,
1416            total_eligible_stake,
1417            validator_scores,
1418        });
1419    }
1420
1421    /// Take the epoch rewards initialization request, clearing it.
1422    ///
1423    /// This is called by the Bank after transaction execution to check if epoch
1424    /// rewards init was requested. The Bank uses the returned data to create
1425    /// the EpochRewards account.
1426    ///
1427    /// Returns `Some(request)` if a request was pending, `None` otherwise.
1428    /// After this call, `epoch_rewards_init` will be `None`.
1429    pub fn take_epoch_rewards_init_request(&self) -> Option<EpochRewardsInitRequest> {
1430        // Take and return the request data
1431        self.epoch_rewards_init
1432            .write()
1433            .expect("Failed to acquire lock")
1434            .take()
1435    }
1436
1437    /// Check if an epoch rewards initialization request is pending.
1438    ///
1439    /// This is used by DistributeRewards to fail if a signal is already set
1440    /// for the current block (prevents multiple DistributeRewards in same block).
1441    ///
1442    /// Returns `true` if a request is pending, `false` otherwise.
1443    /// Does NOT consume the request (unlike `take_epoch_rewards_init_request`).
1444    pub fn is_epoch_rewards_init_pending(&self) -> bool {
1445        self.epoch_rewards_init
1446            .read()
1447            .expect("Failed to acquire lock")
1448            .is_some()
1449    }
1450
1451    /// Get completed frozen epochs (excludes the last/current epoch).
1452    ///
1453    /// Returns epoch numbers for all frozen entries except the last one,
1454    /// which represents the currently ongoing epoch. These are epochs
1455    /// that have completed and are eligible for reward distribution.
1456    ///
1457    /// Returns empty if frozen has 0 or 1 entries (need at least 2 to have completed epochs).
1458    pub fn completed_frozen_epochs(&self) -> Vec<Epoch> {
1459        let frozen_data = self.frozen.read();
1460        let len = frozen_data.len();
1461        if len < 2 {
1462            return vec![];
1463        }
1464        frozen_data
1465            .iter()
1466            .take(len - 1) // Exclude last (current epoch)
1467            .map(|data| data.epoch)
1468            .collect()
1469    }
1470
1471    // ========== Validator Reference Checking ==========
1472
1473    /// Check if any stake account references the given validator pubkey whose
1474    /// unbonding period is NOT yet complete.
1475    ///
1476    /// This performs an O(n) search over all stake accounts starting from
1477    /// pending → frozen → baseline. Uses Rayon's parallel iterator for better
1478    /// performance on multi-core systems.
1479    ///
1480    /// A stake account is considered to "reference" the validator if:
1481    /// - It has `validator == Some(target_validator)`, AND
1482    /// - Either:
1483    ///   - It is **active** (no `deactivation_requested`), OR
1484    ///   - It is **still unbonding** (unbonding conditions not yet met)
1485    ///
1486    /// Stake accounts whose unbonding is complete are NOT considered as referencing
1487    /// the validator, since they can be fully withdrawn or reactivated to another validator.
1488    ///
1489    /// # Unbonding Completion Conditions
1490    ///
1491    /// Unbonding is complete when BOTH conditions are met:
1492    /// 1. **State transition**: `deactivation_timestamp < last_freeze_timestamp`
1493    ///    (at least one FreezeStakes has occurred since deactivation)
1494    /// 2. **Duration enforcement**: `deactivation_timestamp + unbonding_period < current_timestamp`
1495    ///    (the unbonding period has actually elapsed)
1496    ///
1497    /// # Arguments
1498    /// * `validator` - The validator pubkey to check
1499    /// * `validator_info` - The validator's info (used to compute unbonding end via `end_of_unbonding`)
1500    /// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
1501    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1502    ///
1503    /// # Returns
1504    /// `true` if at least one stake account references the validator and is either
1505    /// active or still unbonding, `false` otherwise.
1506    ///
1507    /// # Performance
1508    ///
1509    /// This is an expensive O(n) operation that should only be called when needed
1510    /// (e.g., during Withdraw when checking if a validator can be fully drained).
1511    pub fn is_validator_referenced(
1512        &self,
1513        validator: &Pubkey,
1514        validator_info: &ValidatorInfo,
1515        last_freeze_timestamp: u64,
1516        current_timestamp: u64,
1517    ) -> bool {
1518        // Get all stake accounts and check if any reference the validator using parallel iteration
1519        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1520        all_stake_accounts.par_iter().any(|(_, stake_account)| {
1521            // First check: does this stake reference our target validator?
1522            if stake_account.data.validator.as_ref() != Some(validator) {
1523                return false;
1524            }
1525
1526            // If not deactivating (active stake), it counts as referencing
1527            let Some(deactivation_timestamp) = stake_account.data.deactivation_requested else {
1528                return true;
1529            };
1530
1531            // Check if unbonding is complete using the two-step validation:
1532            // 1. State transition: deactivation must have taken effect
1533            if deactivation_timestamp >= last_freeze_timestamp {
1534                // Still deactivating, counts as referencing
1535                return true;
1536            }
1537
1538            // 2. Duration enforcement: unbonding period must have elapsed
1539            let unbonding_end = validator_info.end_of_unbonding(deactivation_timestamp);
1540
1541            // If unbonding is NOT complete, the stake still counts as referencing
1542            unbonding_end >= current_timestamp
1543        })
1544    }
1545
1546    // ========== Locked Staker Checking ==========
1547
1548    /// Check if any stake account delegated to the given validator is still within
1549    /// its lockup period.
1550    ///
1551    /// This performs an O(n) search over all stake accounts starting from
1552    /// pending → frozen → baseline. Uses Rayon's parallel iterator for better
1553    /// performance on multi-core systems.
1554    ///
1555    /// A staker is considered "locked" if ALL of the following are true:
1556    /// - It has `validator == Some(target_validator)` (delegated to this validator)
1557    /// - It has `activation_requested == Some(timestamp)` (was activated)
1558    /// - `activation_requested + lockup_period > current_timestamp` (lockup hasn't expired)
1559    ///
1560    /// Self-bonds are excluded from lockup checks to prevent the validator from being
1561    /// unable to change commission rates or shut down when only the self-bond exists.
1562    ///
1563    /// # Arguments
1564    /// * `validator` - The validator pubkey to check
1565    /// * `lockup_period` - The validator's lockup period in milliseconds
1566    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1567    ///
1568    /// # Returns
1569    /// `true` if at least one stake account is delegated to the validator and still
1570    /// within its lockup period, `false` otherwise.
1571    pub fn has_locked_stakers(
1572        &self,
1573        validator: &Pubkey,
1574        lockup_period: u64,
1575        current_timestamp: u64,
1576    ) -> bool {
1577        let self_bond_pubkey = derive_self_bond_address(validator);
1578        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1579        all_stake_accounts
1580            .par_iter()
1581            .any(|(pubkey, stake_account)| {
1582                // Skip self-bond PDA
1583                if *pubkey == self_bond_pubkey {
1584                    return false;
1585                }
1586
1587                // First check: does this stake reference our target validator?
1588                if stake_account.data.validator.as_ref() != Some(validator) {
1589                    return false;
1590                }
1591
1592                // Must have been activated to have a lockup
1593                let Some(activation_requested) = stake_account.data.activation_requested else {
1594                    return false;
1595                };
1596
1597                // Check if the lockup period hasn't expired yet
1598                let lockup_end = activation_requested.saturating_add(lockup_period);
1599                lockup_end > current_timestamp
1600            })
1601    }
1602
1603    /// Check if a validator is referenced by any stake accounts (excluding the self-bond).
1604    ///
1605    /// This variant excludes the self-bond PDA from the check to prevent circular logic
1606    /// where the self-bond cannot be deactivated because its existence always makes
1607    /// is_validator_referenced() return true.
1608    ///
1609    /// # Arguments
1610    /// * `validator_pubkey` - The validator pubkey to check
1611    /// * `validator_info` - The validator's info (used to compute unbonding end via `end_of_unbonding`)
1612    /// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
1613    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1614    ///
1615    /// # Returns
1616    /// `true` if at least one non-self-bond stake account references the validator
1617    pub fn is_validator_referenced_excluding_self_bond(
1618        &self,
1619        validator_pubkey: &Pubkey,
1620        validator_info: &ValidatorInfo,
1621        last_freeze_timestamp: u64,
1622        current_timestamp: u64,
1623    ) -> bool {
1624        let self_bond_pubkey = derive_self_bond_address(validator_pubkey);
1625        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1626        all_stake_accounts
1627            .par_iter()
1628            .any(|(pubkey, stake_account)| {
1629                // Skip self-bond PDA
1630                if *pubkey == self_bond_pubkey {
1631                    return false;
1632                }
1633
1634                // First check: does this stake reference our target validator?
1635                if stake_account.data.validator.as_ref() != Some(validator_pubkey) {
1636                    return false;
1637                }
1638
1639                // If not deactivating (active stake), it counts as referencing
1640                let Some(deactivation_timestamp) = stake_account.data.deactivation_requested else {
1641                    return true;
1642                };
1643
1644                // Check if unbonding is complete using the two-step validation:
1645                // 1. State transition: deactivation must have taken effect
1646                if deactivation_timestamp >= last_freeze_timestamp {
1647                    // Still deactivating, counts as referencing
1648                    return true;
1649                }
1650
1651                // 2. Duration enforcement: unbonding period must have elapsed
1652                let unbonding_end = validator_info.end_of_unbonding(deactivation_timestamp);
1653
1654                // If unbonding is NOT complete, the stake still counts as referencing
1655                unbonding_end >= current_timestamp
1656            })
1657    }
1658
1659    // ========== Pending Cache Mutation Accessors ==========
1660
1661    /// Insert a stake account into the pending cache.
1662    pub fn insert_stake_account(&self, pubkey: Pubkey, account: StakeAccount) {
1663        self.pending.insert_stake_account(pubkey, account);
1664    }
1665
1666    /// Insert a validator account into the pending cache.
1667    pub fn insert_validator_account(&self, pubkey: Pubkey, account: ValidatorAccount) {
1668        self.pending.insert_validator_account(pubkey, account);
1669    }
1670
1671    // ========== Adoption Tracking ==========
1672
1673    /// Check if the previous (most recent) frozen epoch has been adopted by consensus.
1674    ///
1675    /// Returns `true` if:
1676    /// - The frozen deque is empty (no previous epoch to adopt), OR
1677    /// - `frozen.back().consensus_adopted_at` is `Some(_)` (adopted)
1678    ///
1679    /// Returns `false` when the last frozen entry exists but hasn't been adopted yet.
1680    /// Used by the FreezeStakes guard to enforce the no-skip invariant.
1681    pub fn is_previous_epoch_adopted(&self) -> bool {
1682        let frozen_data = self.frozen.read();
1683        match frozen_data.back() {
1684            None => true, // No frozen epochs — nothing to adopt
1685            Some(last) => last.consensus_adopted_at.is_some(),
1686        }
1687    }
1688
1689    /// Set `consensus_adopted_at` on the newest frozen entry (back of deque).
1690    ///
1691    /// This is called from `Bank::finalize_impl()` after `take_handover()` returns
1692    /// a timestamp, and from `RequestConsensusEpochChange` in testing mode for
1693    /// auto-adoption.
1694    ///
1695    /// No-op if the frozen deque is empty.
1696    pub fn set_consensus_adopted_at(&self, ts: u64) {
1697        let mut frozen_data = self.frozen.write_lock();
1698        if let Some(last) = frozen_data.back_mut() {
1699            last.consensus_adopted_at = Some(ts);
1700        }
1701    }
1702
1703    /// Signal that a Handover admin transaction was detected in the current block.
1704    ///
1705    /// Called by the ExecutionEngine after `execute_blob()` when it finds an
1706    /// `AdminTransaction::Handover` in the block's admin transactions.
1707    /// The timestamp is consumed by `take_handover()` in `Bank::finalize_impl()`.
1708    pub fn signal_handover(&self, ts: u64) {
1709        *self.handover_ts.write().expect("Failed to acquire lock") = Some(ts);
1710    }
1711
1712    /// Atomically take the handover signal, returning the timestamp if set.
1713    ///
1714    /// Called by `Bank::finalize_impl()` to consume the signal and record
1715    /// adoption on `frozen.back()`.
1716    ///
1717    /// Returns `Some(ts)` if `signal_handover()` was called, `None` otherwise.
1718    /// After this call, the signal is cleared.
1719    pub fn take_handover(&self) -> Option<u64> {
1720        self.handover_ts
1721            .write()
1722            .expect("Failed to acquire lock")
1723            .take()
1724    }
1725}
1726
1727// ========== Read-Only View ==========
1728
1729/// Read-only view of the stake cache for external consumers (e.g., RPC handlers).
1730///
1731/// This type wraps a `StakesHandle` and exposes only read-only query methods.
1732/// Mutation methods (`insert_stake_account`, `insert_validator_account`, `freeze_stakes`,
1733/// `request_epoch_rewards_init`, etc.) are intentionally not exposed.
1734///
1735/// # Usage
1736///
1737/// External code (outside the `svm-execution` crate) should use `Bank::stakes_view()`
1738/// to obtain a `StakesView` instead of accessing the full `StakesHandle` directly.
1739/// This prevents accidental state corruption from RPC handlers or other non-transaction
1740/// code paths.
1741pub struct StakesView(StakesHandle);
1742
1743impl StakesView {
1744    /// Create a new read-only view from a `StakesHandle`.
1745    pub fn new(handle: StakesHandle) -> Self {
1746        Self(handle)
1747    }
1748
1749    // ========== Layered Lookups from Pending ==========
1750
1751    /// Get a stake account starting from pending (next epoch state).
1752    ///
1753    /// Searches: pending → frozen (newest to oldest) → baseline
1754    pub fn get_stake_account_from_pending(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
1755        self.0.get_stake_account_from_pending(pubkey)
1756    }
1757
1758    /// Get a validator account starting from pending (next epoch state).
1759    ///
1760    /// Searches: pending → frozen (newest to oldest) → baseline
1761    pub fn get_validator_account_from_pending(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
1762        self.0.get_validator_account_from_pending(pubkey)
1763    }
1764
1765    /// Get all validator accounts starting from pending (next epoch state).
1766    pub fn get_all_validator_accounts_from_pending(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1767        self.0.get_all_validator_accounts_from_pending()
1768    }
1769
1770    // ========== Layered Lookups from Last Frozen ==========
1771
1772    /// Get a stake account starting from the last frozen epoch (current epoch state).
1773    ///
1774    /// Searches: frozen (newest to oldest) → baseline. Skips pending.
1775    pub fn get_stake_account_from_last_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
1776        self.0.get_stake_account_from_last_frozen(pubkey)
1777    }
1778
1779    /// Get a validator account starting from the last frozen epoch (current epoch state).
1780    ///
1781    /// Searches: frozen (newest to oldest) → baseline. Skips pending.
1782    pub fn get_validator_account_from_last_frozen(
1783        &self,
1784        pubkey: &Pubkey,
1785    ) -> Option<ValidatorAccount> {
1786        self.0.get_validator_account_from_last_frozen(pubkey)
1787    }
1788
1789    /// Get all validator accounts from the last frozen epoch (current epoch state).
1790    pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1791        self.0.get_all_validator_accounts_from_last_frozen()
1792    }
1793
1794    /// Find a validator by their authority key from the last frozen epoch (current epoch state).
1795    ///
1796    /// Performs a layered merge (baseline + frozen deltas, skips pending) and scans
1797    /// for a matching `authority_key`. Avoids the Vec allocation and sort of
1798    /// `get_all_validator_accounts_from_last_frozen()`.
1799    pub fn find_validator_by_authority_key_from_last_frozen(
1800        &self,
1801        authority_key: &[u8],
1802    ) -> Option<ValidatorInfo> {
1803        self.0
1804            .find_validator_by_authority_key_from_last_frozen(authority_key)
1805    }
1806
1807    // ========== Timestamp Accessors ==========
1808
1809    /// Get the timestamp of the last frozen epoch (current epoch's effective state).
1810    ///
1811    /// Returns `None` if no frozen snapshots exist yet.
1812    pub fn last_frozen_timestamp(&self) -> Option<u64> {
1813        self.0.last_frozen_timestamp()
1814    }
1815
1816    /// Get the epoch number of the last frozen snapshot (current epoch).
1817    /// Returns `None` if no frozen snapshots exist yet.
1818    pub fn last_frozen_epoch(&self) -> Option<Epoch> {
1819        self.0.last_frozen_epoch()
1820    }
1821
1822    /// Get the epoch of the pending (next) stake cache.
1823    pub fn pending_epoch(&self) -> Epoch {
1824        self.0.pending_epoch()
1825    }
1826
1827    /// Get the timestamp of the pending stake cache.
1828    pub fn pending_timestamp(&self) -> u64 {
1829        self.0.pending_timestamp()
1830    }
1831}
1832
1833// ========== Test-only accessors ==========
1834#[cfg(test)]
1835impl StakesHandle {
1836    /// Get direct access to baseline for test assertions.
1837    pub fn raw_baseline(&self) -> &StakeCache {
1838        &self.baseline
1839    }
1840
1841    /// Get direct access to pending for test assertions.
1842    pub fn raw_pending(&self) -> &StakeCache {
1843        &self.pending
1844    }
1845
1846    /// Get direct access to frozen for test assertions.
1847    pub fn raw_frozen(&self) -> &StakeHistory {
1848        &self.frozen
1849    }
1850}
1851
1852#[cfg(test)]
1853mod tests {
1854    use rialo_stake_manager_interface::instruction::StakeInfo;
1855    use rialo_validator_registry_interface::instruction::ValidatorInfo;
1856
1857    use super::*;
1858
1859    // ========================================================================
1860    // Test Helper Functions
1861    // ========================================================================
1862
1863    fn create_test_stake_account(kelvins: u64, validator: Pubkey) -> StakeAccount {
1864        StakeAccount {
1865            kelvins,
1866            data: StakeInfo {
1867                activation_requested: Some(0),
1868                deactivation_requested: None,
1869                delegated_balance: kelvins,
1870                validator: Some(validator),
1871                admin_authority: Pubkey::new_unique(),
1872                withdraw_authority: Pubkey::new_unique(),
1873                reward_receiver: None,
1874            },
1875        }
1876    }
1877
1878    fn create_test_validator_account(kelvins: u64, stake: u64) -> ValidatorAccount {
1879        ValidatorAccount {
1880            kelvins,
1881            data: ValidatorInfo {
1882                signing_key: Pubkey::new_unique(),
1883                withdrawal_key: Pubkey::new_unique(),
1884                registration_time: 0,
1885                stake,
1886                address: vec![],
1887                subdag_sync_address: vec![],
1888                network_service_address: vec![],
1889                hostname: String::new(),
1890                authority_key: vec![0u8; 96],
1891                protocol_key: Pubkey::new_unique(),
1892                network_key: Pubkey::new_unique(),
1893                last_update: 0,
1894                unbonding_periods: std::collections::BTreeMap::from([(0, 0)]),
1895                lockup_period: 0,
1896                commission_rate: 500,
1897                new_commission_rate: None,
1898                earliest_shutdown: None,
1899            },
1900        }
1901    }
1902
1903    // ========================================================================
1904    // Layered Lookup Tests: pending → frozen → baseline
1905    // ========================================================================
1906
1907    #[test]
1908    fn test_layered_lookup_stake_account_from_pending() {
1909        let pubkey = Pubkey::new_unique();
1910        let validator = Pubkey::new_unique();
1911        let handle = StakesHandle::default();
1912
1913        // Insert into pending
1914        let pending_account = create_test_stake_account(1000, validator);
1915        handle.insert_stake_account(pubkey, pending_account.clone());
1916
1917        // Lookup should find in pending
1918        let found = handle.get_stake_account_from_pending(&pubkey);
1919        assert!(found.is_some());
1920        assert_eq!(found.unwrap().kelvins, 1000);
1921    }
1922
1923    #[test]
1924    fn test_layered_lookup_stake_account_from_frozen() {
1925        let pubkey = Pubkey::new_unique();
1926        let validator = Pubkey::new_unique();
1927        let handle = StakesHandle::default();
1928
1929        // Insert into pending and freeze
1930        let account = create_test_stake_account(2000, validator);
1931        handle.insert_stake_account(pubkey, account);
1932        handle.freeze_stakes();
1933
1934        // Account should now be in frozen, pending should be empty
1935        let found = handle.get_stake_account_from_pending(&pubkey);
1936        assert!(found.is_some());
1937        assert_eq!(found.unwrap().kelvins, 2000);
1938
1939        // Confirm pending is empty
1940        assert!(handle.raw_pending().get_stake_account(&pubkey).is_none());
1941    }
1942
1943    #[test]
1944    fn test_layered_lookup_stake_account_from_baseline() {
1945        let pubkey = Pubkey::new_unique();
1946        let validator = Pubkey::new_unique();
1947
1948        // Create a handle with account in baseline
1949        let mut baseline_data = StakeCacheData::default();
1950        baseline_data
1951            .stake_accounts
1952            .insert(pubkey, Some(create_test_stake_account(3000, validator)));
1953        let baseline = StakeCache::with_data(baseline_data);
1954        let handle = StakesHandle::new_shared(
1955            baseline,
1956            StakeCache::default(),
1957            StakeHistory::default(),
1958            Arc::new(|_| false),
1959        );
1960
1961        // Lookup should find in baseline
1962        let found = handle.get_stake_account_from_pending(&pubkey);
1963        assert!(found.is_some());
1964        assert_eq!(found.unwrap().kelvins, 3000);
1965    }
1966
1967    #[test]
1968    fn test_layered_lookup_priority_pending_over_frozen() {
1969        let pubkey = Pubkey::new_unique();
1970        let validator = Pubkey::new_unique();
1971        let handle = StakesHandle::default();
1972
1973        // Insert into pending with value 1000
1974        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
1975        // Freeze it
1976        handle.freeze_stakes();
1977
1978        // Insert into pending again with value 2000 (overwrites for next epoch)
1979        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
1980
1981        // Lookup from pending should find 2000 (pending wins)
1982        let found = handle.get_stake_account_from_pending(&pubkey);
1983        assert!(found.is_some());
1984        assert_eq!(found.unwrap().kelvins, 2000);
1985
1986        // Lookup from last frozen should find 1000 (skips pending)
1987        let found_frozen = handle.get_stake_account_from_last_frozen(&pubkey);
1988        assert!(found_frozen.is_some());
1989        assert_eq!(found_frozen.unwrap().kelvins, 1000);
1990    }
1991
1992    #[test]
1993    fn test_layered_lookup_priority_frozen_over_baseline() {
1994        let pubkey = Pubkey::new_unique();
1995        let validator = Pubkey::new_unique();
1996
1997        // Create baseline with value 1000
1998        let mut baseline_data = StakeCacheData::default();
1999        baseline_data
2000            .stake_accounts
2001            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
2002        let baseline = StakeCache::with_data(baseline_data);
2003        let handle = StakesHandle::new_shared(
2004            baseline,
2005            StakeCache::default(),
2006            StakeHistory::default(),
2007            Arc::new(|_| false),
2008        );
2009
2010        // Insert into pending with value 2000 and freeze
2011        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
2012        handle.freeze_stakes();
2013
2014        // Lookup should find 2000 (frozen wins over baseline)
2015        let found = handle.get_stake_account_from_pending(&pubkey);
2016        assert!(found.is_some());
2017        assert_eq!(found.unwrap().kelvins, 2000);
2018    }
2019
2020    #[test]
2021    fn test_layered_lookup_multiple_frozen_epochs() {
2022        let pubkey = Pubkey::new_unique();
2023        let validator = Pubkey::new_unique();
2024        let handle = StakesHandle::default();
2025
2026        // Epoch 1: Insert and freeze with value 1000
2027        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
2028        handle.freeze_stakes();
2029
2030        // Epoch 2: Insert and freeze with value 2000
2031        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
2032        handle.freeze_stakes();
2033
2034        // Epoch 3: Insert and freeze with value 3000
2035        handle.insert_stake_account(pubkey, create_test_stake_account(3000, validator));
2036        handle.freeze_stakes();
2037
2038        // Lookup from last frozen should find 3000 (newest frozen)
2039        let found = handle.get_stake_account_from_last_frozen(&pubkey);
2040        assert!(found.is_some());
2041        assert_eq!(found.unwrap().kelvins, 3000);
2042
2043        // Verify frozen history has 3 entries
2044        assert_eq!(handle.frozen_len(), 3);
2045    }
2046
2047    #[test]
2048    fn test_layered_lookup_validator_account() {
2049        let pubkey = Pubkey::new_unique();
2050
2051        // Create baseline with validator
2052        let mut baseline_data = StakeCacheData::default();
2053        baseline_data
2054            .validator_accounts
2055            .insert(pubkey, Some(create_test_validator_account(1000, 500)));
2056        let baseline = StakeCache::with_data(baseline_data);
2057        let handle = StakesHandle::new_shared(
2058            baseline,
2059            StakeCache::default(),
2060            StakeHistory::default(),
2061            Arc::new(|_| false),
2062        );
2063
2064        // Lookup should find in baseline
2065        let found = handle.get_validator_account_from_pending(&pubkey);
2066        assert!(found.is_some());
2067        assert_eq!(found.unwrap().kelvins, 1000);
2068
2069        // Add update in pending
2070        handle.insert_validator_account(pubkey, create_test_validator_account(2000, 600));
2071
2072        // Lookup should now find pending value
2073        let found = handle.get_validator_account_from_pending(&pubkey);
2074        assert!(found.is_some());
2075        assert_eq!(found.unwrap().kelvins, 2000);
2076    }
2077
2078    // ========================================================================
2079    // Tombstone Handling Tests
2080    // ========================================================================
2081
2082    #[test]
2083    fn test_tombstone_in_pending_hides_frozen() {
2084        let pubkey = Pubkey::new_unique();
2085        let validator = Pubkey::new_unique();
2086        let handle = StakesHandle::default();
2087
2088        // Insert and freeze
2089        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
2090        handle.freeze_stakes();
2091
2092        // Add tombstone in pending (marks as deleted for next epoch)
2093        handle.raw_pending().tombstone_stake_account(pubkey);
2094
2095        // Lookup from pending should return None (tombstone = deleted)
2096        let found = handle.get_stake_account_from_pending(&pubkey);
2097        assert!(
2098            found.is_none(),
2099            "Tombstone in pending should hide frozen value"
2100        );
2101
2102        // Lookup from last frozen should still find the value (skips pending)
2103        let found_frozen = handle.get_stake_account_from_last_frozen(&pubkey);
2104        assert!(found_frozen.is_some());
2105        assert_eq!(found_frozen.unwrap().kelvins, 1000);
2106    }
2107
2108    #[test]
2109    fn test_tombstone_in_frozen_hides_baseline() {
2110        let pubkey = Pubkey::new_unique();
2111        let validator = Pubkey::new_unique();
2112
2113        // Create baseline with account
2114        let mut baseline_data = StakeCacheData::default();
2115        baseline_data
2116            .stake_accounts
2117            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
2118        let baseline = StakeCache::with_data(baseline_data);
2119        let handle = StakesHandle::new_shared(
2120            baseline,
2121            StakeCache::default(),
2122            StakeHistory::default(),
2123            Arc::new(|_| false),
2124        );
2125
2126        // Add tombstone in pending and freeze
2127        handle.raw_pending().tombstone_stake_account(pubkey);
2128        handle.freeze_stakes();
2129
2130        // Lookup from last frozen should return None (tombstone hides baseline)
2131        let found = handle.get_stake_account_from_last_frozen(&pubkey);
2132        assert!(
2133            found.is_none(),
2134            "Tombstone in frozen should hide baseline value"
2135        );
2136
2137        // First frozen lookup should also see tombstone
2138        let found_first = handle.get_stake_account_from_first_frozen(&pubkey);
2139        assert!(found_first.is_none());
2140    }
2141
2142    #[test]
2143    fn test_tombstone_validator_account() {
2144        let pubkey = Pubkey::new_unique();
2145
2146        // Create baseline with validator
2147        let mut baseline_data = StakeCacheData::default();
2148        baseline_data
2149            .validator_accounts
2150            .insert(pubkey, Some(create_test_validator_account(1000, 500)));
2151        let baseline = StakeCache::with_data(baseline_data);
2152        let handle = StakesHandle::new_shared(
2153            baseline,
2154            StakeCache::default(),
2155            StakeHistory::default(),
2156            Arc::new(|_| false),
2157        );
2158
2159        // Lookup should find in baseline initially
2160        assert!(handle.get_validator_account_from_pending(&pubkey).is_some());
2161
2162        // Add tombstone in pending
2163        handle.raw_pending().tombstone_validator_account(pubkey);
2164
2165        // Lookup from pending should now return None
2166        let found = handle.get_validator_account_from_pending(&pubkey);
2167        assert!(found.is_none(), "Tombstone should hide baseline validator");
2168    }
2169
2170    #[test]
2171    fn test_get_all_validators_excludes_tombstones() {
2172        let pubkey1 = Pubkey::new_unique();
2173        let pubkey2 = Pubkey::new_unique();
2174
2175        // Create baseline with two validators
2176        let mut baseline_data = StakeCacheData::default();
2177        baseline_data
2178            .validator_accounts
2179            .insert(pubkey1, Some(create_test_validator_account(1000, 100)));
2180        baseline_data
2181            .validator_accounts
2182            .insert(pubkey2, Some(create_test_validator_account(2000, 200)));
2183        let baseline = StakeCache::with_data(baseline_data);
2184        let handle = StakesHandle::new_shared(
2185            baseline,
2186            StakeCache::default(),
2187            StakeHistory::default(),
2188            Arc::new(|_| false),
2189        );
2190
2191        // Initially should have 2 validators
2192        let all = handle.get_all_validator_accounts_from_pending();
2193        assert_eq!(all.len(), 2);
2194
2195        // Add tombstone for pubkey1 in pending
2196        handle.raw_pending().tombstone_validator_account(pubkey1);
2197
2198        // Now should only have 1 validator (pubkey2)
2199        let all = handle.get_all_validator_accounts_from_pending();
2200        assert_eq!(all.len(), 1);
2201        assert_eq!(all[0].0, pubkey2);
2202    }
2203
2204    #[test]
2205    fn test_tombstone_then_readd() {
2206        let pubkey = Pubkey::new_unique();
2207        let validator = Pubkey::new_unique();
2208
2209        // Create baseline with account
2210        let mut baseline_data = StakeCacheData::default();
2211        baseline_data
2212            .stake_accounts
2213            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
2214        let baseline = StakeCache::with_data(baseline_data);
2215        let handle = StakesHandle::new_shared(
2216            baseline,
2217            StakeCache::default(),
2218            StakeHistory::default(),
2219            Arc::new(|_| false),
2220        );
2221
2222        // Delete in epoch 1
2223        handle.raw_pending().tombstone_stake_account(pubkey);
2224        handle.freeze_stakes();
2225
2226        // Should be deleted
2227        let found = handle.get_stake_account_from_last_frozen(&pubkey);
2228        assert!(found.is_none());
2229
2230        // Re-add in epoch 2 with new value
2231        handle.insert_stake_account(pubkey, create_test_stake_account(5000, validator));
2232        handle.freeze_stakes();
2233
2234        // Should be visible again with new value
2235        let found = handle.get_stake_account_from_last_frozen(&pubkey);
2236        assert!(found.is_some());
2237        assert_eq!(found.unwrap().kelvins, 5000);
2238    }
2239
2240    // ========================================================================
2241    // Empty Epoch Handling Tests
2242    // ========================================================================
2243
2244    #[test]
2245    fn test_empty_pending_freeze() {
2246        let handle = StakesHandle::default();
2247
2248        // Freeze with empty pending
2249        handle.freeze_stakes();
2250
2251        // Frozen should have 1 entry (empty delta)
2252        assert_eq!(handle.frozen_len(), 1);
2253
2254        // Lookup should still work (returns None for nonexistent)
2255        let pubkey = Pubkey::new_unique();
2256        assert!(handle.get_stake_account_from_pending(&pubkey).is_none());
2257    }
2258
2259    #[test]
2260    fn test_empty_frozen_epochs() {
2261        let pubkey = Pubkey::new_unique();
2262        let validator = Pubkey::new_unique();
2263
2264        // Create baseline with account
2265        let mut baseline_data = StakeCacheData::default();
2266        baseline_data
2267            .stake_accounts
2268            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
2269        let baseline = StakeCache::with_data(baseline_data);
2270        let handle = StakesHandle::new_shared(
2271            baseline,
2272            StakeCache::default(),
2273            StakeHistory::default(),
2274            Arc::new(|_| false),
2275        );
2276
2277        // Freeze several empty epochs
2278        handle.freeze_stakes();
2279        handle.freeze_stakes();
2280        handle.freeze_stakes();
2281
2282        // Lookup should still find baseline value through empty frozen epochs
2283        let found = handle.get_stake_account_from_pending(&pubkey);
2284        assert!(found.is_some());
2285        assert_eq!(found.unwrap().kelvins, 1000);
2286    }
2287
2288    #[test]
2289    fn test_no_frozen_epochs_falls_through_to_baseline() {
2290        let pubkey = Pubkey::new_unique();
2291        let validator = Pubkey::new_unique();
2292
2293        // Create baseline with account, no frozen history
2294        let mut baseline_data = StakeCacheData::default();
2295        baseline_data
2296            .stake_accounts
2297            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
2298        let baseline = StakeCache::with_data(baseline_data);
2299        let handle = StakesHandle::new_shared(
2300            baseline,
2301            StakeCache::default(),
2302            StakeHistory::default(),
2303            Arc::new(|_| false),
2304        );
2305
2306        // Lookup from last frozen should fall through to baseline
2307        let found = handle.get_stake_account_from_last_frozen(&pubkey);
2308        assert!(found.is_some());
2309        assert_eq!(found.unwrap().kelvins, 1000);
2310    }
2311
2312    #[test]
2313    fn test_get_all_stake_accounts_from_frozen_epoch() {
2314        // Test that from_frozen_epoch only includes deltas up to the target epoch
2315        let validator = Pubkey::new_unique();
2316
2317        // Baseline: one account
2318        let baseline_stake = Pubkey::new_unique();
2319        let mut baseline_data = StakeCacheData::default();
2320        baseline_data.stake_accounts.insert(
2321            baseline_stake,
2322            Some(create_test_stake_account(1000, validator)),
2323        );
2324        let baseline = StakeCache::with_data(baseline_data);
2325        let handle = StakesHandle::new_shared(
2326            baseline,
2327            StakeCache::default(),
2328            StakeHistory::default(),
2329            Arc::new(|_| false),
2330        );
2331
2332        // Epoch 5: Add stake_epoch5
2333        let stake_epoch5 = Pubkey::new_unique();
2334        handle.set_pending_epoch(5);
2335        handle.insert_stake_account(stake_epoch5, create_test_stake_account(2000, validator));
2336        handle.freeze_stakes();
2337
2338        // Epoch 6: Add stake_epoch6
2339        let stake_epoch6 = Pubkey::new_unique();
2340        handle.insert_stake_account(stake_epoch6, create_test_stake_account(3000, validator));
2341        handle.freeze_stakes();
2342
2343        // Epoch 7: Add stake_epoch7
2344        let stake_epoch7 = Pubkey::new_unique();
2345        handle.insert_stake_account(stake_epoch7, create_test_stake_account(4000, validator));
2346        handle.freeze_stakes();
2347
2348        // Verify: from_frozen_epoch(5) should include baseline + epoch 5 only
2349        let accounts_epoch5 = handle.get_all_stake_accounts_from_frozen_epoch(5);
2350        assert_eq!(accounts_epoch5.len(), 2); // baseline + epoch5
2351        assert!(accounts_epoch5.iter().any(|(k, _)| *k == baseline_stake));
2352        assert!(accounts_epoch5.iter().any(|(k, _)| *k == stake_epoch5));
2353        assert!(!accounts_epoch5.iter().any(|(k, _)| *k == stake_epoch6));
2354
2355        // Verify: from_frozen_epoch(6) should include baseline + epoch 5 + epoch 6
2356        let accounts_epoch6 = handle.get_all_stake_accounts_from_frozen_epoch(6);
2357        assert_eq!(accounts_epoch6.len(), 3);
2358        assert!(accounts_epoch6.iter().any(|(k, _)| *k == stake_epoch6));
2359        assert!(!accounts_epoch6.iter().any(|(k, _)| *k == stake_epoch7));
2360
2361        // Verify: from_frozen_epoch(7) should include all 4
2362        let accounts_epoch7 = handle.get_all_stake_accounts_from_frozen_epoch(7);
2363        assert_eq!(accounts_epoch7.len(), 4);
2364        assert!(accounts_epoch7.iter().any(|(k, _)| *k == stake_epoch7));
2365    }
2366
2367    #[test]
2368    fn get_all_validator_accounts_from_frozen_epoch_layered_with_tombstone() {
2369        // Build a small layered cache and verify the new method walks
2370        // baseline + frozen deltas up to `target_epoch`, honours
2371        // tombstones, and returns results sorted by pubkey ascending.
2372        let baseline_v = Pubkey::new_unique();
2373        let epoch5_v = Pubkey::new_unique();
2374        let epoch6_v = Pubkey::new_unique();
2375
2376        let mut baseline_data = StakeCacheData::default();
2377        baseline_data
2378            .validator_accounts
2379            .insert(baseline_v, Some(create_test_validator_account(1000, 100)));
2380        let baseline = StakeCache::with_data(baseline_data);
2381        let handle = StakesHandle::new_shared(
2382            baseline,
2383            StakeCache::default(),
2384            StakeHistory::default(),
2385            Arc::new(|_| false),
2386        );
2387
2388        handle.set_pending_epoch(5);
2389        handle.insert_validator_account(epoch5_v, create_test_validator_account(2000, 200));
2390        handle.freeze_stakes();
2391
2392        handle.insert_validator_account(epoch6_v, create_test_validator_account(3000, 300));
2393        // Tombstone the baseline validator in epoch 6's delta.
2394        handle.raw_pending().tombstone_validator_account(baseline_v);
2395        handle.freeze_stakes();
2396
2397        // Up to epoch 5: baseline + epoch5_v only.
2398        let snapshot_5 = handle.get_all_validator_accounts_from_frozen_epoch(5);
2399        let keys_5: Vec<_> = snapshot_5.iter().map(|(k, _)| *k).collect();
2400        assert!(keys_5.contains(&baseline_v));
2401        assert!(keys_5.contains(&epoch5_v));
2402        assert!(!keys_5.contains(&epoch6_v));
2403
2404        // Up to epoch 6: tombstone hides baseline_v, epoch6_v is now visible.
2405        let snapshot_6 = handle.get_all_validator_accounts_from_frozen_epoch(6);
2406        let keys_6: Vec<_> = snapshot_6.iter().map(|(k, _)| *k).collect();
2407        assert!(!keys_6.contains(&baseline_v));
2408        assert!(keys_6.contains(&epoch5_v));
2409        assert!(keys_6.contains(&epoch6_v));
2410
2411        // Sorted ascending by pubkey bytes.
2412        let mut sorted = keys_6.clone();
2413        sorted.sort();
2414        assert_eq!(keys_6, sorted);
2415    }
2416
2417    #[test]
2418    fn test_get_all_validators_with_no_validators() {
2419        let handle = StakesHandle::default();
2420
2421        // No validators anywhere
2422        let all = handle.get_all_validator_accounts_from_pending();
2423        assert!(all.is_empty());
2424
2425        // Freeze and check again
2426        handle.freeze_stakes();
2427        let all = handle.get_all_validator_accounts_from_last_frozen();
2428        assert!(all.is_empty());
2429    }
2430
2431    // ========== Adoption Tracking Tests ==========
2432
2433    #[test]
2434    fn test_is_previous_epoch_adopted_empty_deque() {
2435        let handle = StakesHandle::default();
2436        // Empty frozen deque → no previous epoch to adopt → returns true
2437        assert!(handle.is_previous_epoch_adopted());
2438    }
2439
2440    #[test]
2441    fn test_is_previous_epoch_adopted_unadopted() {
2442        let handle = StakesHandle::default();
2443        // Push a frozen entry with consensus_adopted_at: None (default)
2444        handle.freeze_stakes();
2445        assert!(
2446            !handle.is_previous_epoch_adopted(),
2447            "Frozen entry with consensus_adopted_at=None should NOT be considered adopted"
2448        );
2449    }
2450
2451    #[test]
2452    fn test_is_previous_epoch_adopted_adopted() {
2453        let handle = StakesHandle::default();
2454        handle.freeze_stakes();
2455        handle.set_consensus_adopted_at(100);
2456        assert!(
2457            handle.is_previous_epoch_adopted(),
2458            "Frozen entry with consensus_adopted_at=Some(100) should be considered adopted"
2459        );
2460    }
2461
2462    #[test]
2463    fn test_is_previous_epoch_adopted_genesis() {
2464        let handle = StakesHandle::default();
2465        // Simulate genesis: set adoption on pending, then freeze
2466        handle.raw_pending().write().consensus_adopted_at = Some(0);
2467        handle.freeze_stakes();
2468        assert!(
2469            handle.is_previous_epoch_adopted(),
2470            "Genesis epoch with consensus_adopted_at=Some(0) should be considered adopted"
2471        );
2472    }
2473
2474    #[test]
2475    fn test_set_consensus_adopted_at() {
2476        let handle = StakesHandle::default();
2477        handle.freeze_stakes();
2478
2479        // Before setting, should be None
2480        assert_eq!(
2481            handle.raw_frozen().back().unwrap().consensus_adopted_at,
2482            None
2483        );
2484
2485        handle.set_consensus_adopted_at(42);
2486
2487        // After setting, should be Some(42)
2488        assert_eq!(
2489            handle.raw_frozen().back().unwrap().consensus_adopted_at,
2490            Some(42)
2491        );
2492    }
2493
2494    #[test]
2495    fn test_signal_and_take_handover() {
2496        let handle = StakesHandle::default();
2497
2498        // Signal a handover timestamp
2499        handle.signal_handover(100);
2500
2501        // First take should return the timestamp
2502        assert_eq!(handle.take_handover(), Some(100));
2503
2504        // Second take should return None (consumed)
2505        assert_eq!(handle.take_handover(), None);
2506    }
2507
2508    #[test]
2509    fn test_take_handover_without_signal() {
2510        let handle = StakesHandle::default();
2511        // No signal was set → take returns None
2512        assert_eq!(handle.take_handover(), None);
2513    }
2514
2515    #[test]
2516    fn test_signal_handover_overwrites() {
2517        let handle = StakesHandle::default();
2518
2519        // Signal twice — last writer wins
2520        handle.signal_handover(100);
2521        handle.signal_handover(200);
2522
2523        assert_eq!(
2524            handle.take_handover(),
2525            Some(200),
2526            "Second signal should overwrite the first"
2527        );
2528        assert_eq!(handle.take_handover(), None);
2529    }
2530
2531    // ========== Eligibility Predicate Tests ==========
2532
2533    fn stake_with(
2534        activation: Option<u64>,
2535        deactivation: Option<u64>,
2536        validator: Option<Pubkey>,
2537    ) -> StakeAccount {
2538        StakeAccount {
2539            kelvins: 1_000,
2540            data: StakeInfo {
2541                activation_requested: activation,
2542                deactivation_requested: deactivation,
2543                delegated_balance: 1_000,
2544                validator,
2545                admin_authority: Pubkey::new_unique(),
2546                withdraw_authority: Pubkey::new_unique(),
2547                reward_receiver: None,
2548            },
2549        }
2550    }
2551
2552    #[test]
2553    fn test_eligibility_activated_no_deactivation() {
2554        let stake = stake_with(Some(0), None, Some(Pubkey::new_unique()));
2555        assert!(is_stake_eligible(&stake, 5));
2556    }
2557
2558    #[test]
2559    fn test_eligibility_not_activated() {
2560        let stake = stake_with(None, None, Some(Pubkey::new_unique()));
2561        assert!(!is_stake_eligible(&stake, 0));
2562    }
2563
2564    #[test]
2565    fn test_eligibility_deactivated_before() {
2566        // deactivation_requested = 1, epoch_timestamp = 2 → 1 < 2 → ineligible
2567        let stake = stake_with(Some(0), Some(1), Some(Pubkey::new_unique()));
2568        assert!(!is_stake_eligible(&stake, 2));
2569    }
2570
2571    #[test]
2572    fn test_eligibility_deactivated_on_boundary_is_eligible() {
2573        // deactivation_requested = 2, epoch_timestamp = 2 → 2 >= 2 → still eligible
2574        let stake = stake_with(Some(0), Some(2), Some(Pubkey::new_unique()));
2575        assert!(is_stake_eligible(&stake, 2));
2576    }
2577
2578    #[test]
2579    fn test_eligibility_no_validator() {
2580        let stake = stake_with(Some(0), None, None);
2581        assert!(!is_stake_eligible(&stake, 0));
2582    }
2583
2584    #[test]
2585    fn test_filter_eligible_stakes_drops_ineligible_and_carries_validator() {
2586        let validator = Pubkey::new_unique();
2587        let eligible_pk = Pubkey::new_unique();
2588        let ineligible_pk = Pubkey::new_unique();
2589        let input = vec![
2590            (eligible_pk, stake_with(Some(0), None, Some(validator))),
2591            (ineligible_pk, stake_with(None, None, Some(validator))),
2592        ];
2593        let eligible = filter_eligible_stakes(input, 0);
2594        assert_eq!(eligible.len(), 1);
2595        assert_eq!(eligible[0].stake_pubkey, eligible_pk);
2596        assert_eq!(eligible[0].validator_pubkey, validator);
2597    }
2598
2599    // ========== Frozen-Epoch Lookup Tests ==========
2600
2601    #[test]
2602    fn test_frozen_epoch_meta_matches_by_epoch_not_front() {
2603        let handle = StakesHandle::default();
2604
2605        // Push frozen entries with epoch 5 (ts 100) and epoch 7 (ts 300).
2606        // `front()` would return epoch 5; this test asserts we lookup by
2607        // epoch number, not position. Picking distinct, non-zero
2608        // `consensus_adopted_at` values on both entries also distinguishes
2609        // a real adopted timestamp from the genesis sentinel `Some(0)`.
2610        let mut e5 = StakeCacheData {
2611            epoch: 5,
2612            timestamp: 100,
2613            ..Default::default()
2614        };
2615        e5.consensus_adopted_at = Some(50);
2616        let mut e7 = StakeCacheData {
2617            epoch: 7,
2618            timestamp: 300,
2619            ..Default::default()
2620        };
2621        e7.consensus_adopted_at = Some(250);
2622        handle.push_frozen(e5);
2623        handle.push_frozen(e7);
2624
2625        assert_eq!(handle.get_frozen_epoch_meta(5), Some((100, Some(50))));
2626        assert_eq!(handle.get_frozen_epoch_meta(7), Some((300, Some(250))));
2627        assert_eq!(handle.get_frozen_epoch_meta(6), None);
2628    }
2629}