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