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