Skip to main content

rialo_stake_cache_interface/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared types for the Stake Cache.
5//!
6//! This crate provides types that are shared between `svm-execution` and
7//! `rialo-s-program-runtime`, allowing builtin programs to access and
8//! manipulate stake cache data during transaction execution.
9//!
10//! ## Reward Distribution Flow
11//!
12//! The reward distribution follows a specific flow:
13//!
14//! 1. **FreezeStakes**: At epoch boundary, push pending to frozen (creates epoch snapshot)
15//! 2. **DistributeRewards**: Creates EpochRewards account (initially inactive/queued)
16//! 3. **Activation**: When EpochRewards becomes active:
17//!    - `pop_front_and_merge_to_baseline()` is called
18//!    - frozen.front() is merged into baseline
19//!    - Rewards are calculated from baseline only
20//! 4. **Distribution**: Rewards distributed across partitions
21//! 5. **Completion**: EpochRewards marked inactive
22//!
23//! ## Reward Eligibility
24//!
25//! Stakes are eligible for rewards based on the following checks:
26//! - `activation_requested.is_some()` → stake was activated
27//! - deactivation not yet effective (timestamp-based check against epoch boundary)
28//! - `validator.is_some()` → stake is delegated to a validator
29//!
30//! ## Lookup Methods
31//!
32//! Different lookup methods for different use cases:
33//!
34//! - **From Pending** (`get_*_from_pending`): Includes next epoch changes
35//! - **From Last Frozen** (`get_*_from_last_frozen`): Current epoch's effective state
36//! - **From First Frozen** (`get_*_from_first_frozen`): Oldest pending rewards epoch
37//! - **From Baseline** (`get_*_from_baseline`): Post-merge state for reward calculation
38
39use std::{
40    collections::{HashMap, HashSet, VecDeque},
41    sync::{
42        atomic::{AtomicBool, Ordering},
43        RwLock,
44    },
45};
46
47use rayon::prelude::*;
48use rialo_s_account::ReadableAccount;
49use rialo_s_clock::Epoch;
50use rialo_s_pubkey::Pubkey;
51use rialo_s_type_overrides::sync::Arc;
52use rialo_stake_manager_interface::instruction::StakeInfo;
53// Re-export ValidatorInfo so downstream crates (e.g., rialo-s-program-runtime) can reference the
54// type without adding a direct dependency on rialo-validator-registry-interface.
55pub use rialo_validator_registry_interface::instruction::ValidatorInfo;
56
57/// PDA derivation helpers for self-bond accounts.
58pub mod pda;
59pub use pda::{derive_self_bond_address, derive_self_bond_address_with_bump, SELF_BOND_SEED};
60
61/// A cache of stake and validator accounts.
62///
63/// This wraps `StakeCacheData` in `Arc<RwLock<...>>` to allow thread-safe shared
64/// access during parallel transaction execution. The Arc allows the same data
65/// to be shared between the Bank and StakesHandle, so mutations to pending
66/// stake data by builtin programs are visible to the Bank.
67#[derive(Debug, Clone)]
68pub struct StakeCache(Arc<RwLock<StakeCacheData>>);
69
70impl Default for StakeCache {
71    fn default() -> Self {
72        Self(Arc::new(RwLock::new(StakeCacheData::default())))
73    }
74}
75
76impl StakeCache {
77    /// Create a new empty stake cache.
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Create a stake cache with the given data.
83    pub fn with_data(data: StakeCacheData) -> Self {
84        Self(Arc::new(RwLock::new(data)))
85    }
86
87    /// Create a stake cache from an existing Arc (for sharing references).
88    pub fn from_arc(arc: Arc<RwLock<StakeCacheData>>) -> Self {
89        Self(arc)
90    }
91
92    /// Get a clone of the inner Arc for sharing.
93    pub fn arc_clone(&self) -> Arc<RwLock<StakeCacheData>> {
94        Arc::clone(&self.0)
95    }
96
97    /// Acquire a read lock on the inner data.
98    pub fn read(&self) -> std::sync::RwLockReadGuard<'_, StakeCacheData> {
99        self.0.read().expect("Failed to acquire read lock")
100    }
101
102    /// Acquire a write lock on the inner data.
103    pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, StakeCacheData> {
104        self.0.write().expect("Failed to acquire write lock")
105    }
106
107    /// Get a stake account by pubkey.
108    ///
109    /// Note: This is a single-layer lookup on just this cache.
110    /// For layered lookup across baseline/frozen/pending, use `StakesHandle::get_stake_account`.
111    pub fn get_stake_account(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
112        let data = self.read();
113        data.stake_accounts.get(pubkey).and_then(|opt| opt.clone())
114    }
115
116    /// Get a validator account by pubkey.
117    ///
118    /// Note: This is a single-layer lookup on just this cache.
119    /// For layered lookup across baseline/frozen/pending, use `StakesHandle::get_validator_account`.
120    pub fn get_validator_account(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
121        let data = self.read();
122        data.validator_accounts
123            .get(pubkey)
124            .and_then(|opt| opt.clone())
125    }
126
127    /// Get all validator accounts from this cache (single layer).
128    ///
129    /// Note: This is a single-layer lookup. For merged view across all layers,
130    /// use `StakesHandle::get_all_validator_accounts`.
131    pub fn get_all_validator_accounts(&self) -> Vec<(Pubkey, ValidatorAccount)> {
132        let data = self.read();
133        data.validator_accounts
134            .iter()
135            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
136            .collect()
137    }
138
139    /// Check if a stake account exists in this cache (single layer).
140    pub fn contains_stake_account(&self, pubkey: &Pubkey) -> bool {
141        let data = self.read();
142        matches!(data.stake_accounts.get(pubkey), Some(Some(_)))
143    }
144
145    /// Check if a validator account exists in this cache (single layer).
146    pub fn contains_validator_account(&self, pubkey: &Pubkey) -> bool {
147        let data = self.read();
148        matches!(data.validator_accounts.get(pubkey), Some(Some(_)))
149    }
150
151    /// Insert or update a stake account.
152    ///
153    /// Also tracks the pubkey as modified for persistence.
154    pub fn insert_stake_account(&self, pubkey: Pubkey, account: StakeAccount) {
155        let mut data = self.write();
156        data.stake_accounts.insert(pubkey, Some(account));
157        data.modified_stake_pubkeys.insert(pubkey);
158    }
159
160    /// Insert or update a validator account.
161    ///
162    /// Also tracks the pubkey as modified for persistence.
163    pub fn insert_validator_account(&self, pubkey: Pubkey, account: ValidatorAccount) {
164        let mut data = self.write();
165        data.validator_accounts.insert(pubkey, Some(account));
166        data.modified_validator_pubkeys.insert(pubkey);
167    }
168
169    /// Insert a tombstone for a stake account (marks as deleted).
170    ///
171    /// Also tracks the pubkey as modified for persistence.
172    pub fn tombstone_stake_account(&self, pubkey: Pubkey) {
173        let mut data = self.write();
174        data.stake_accounts.insert(pubkey, None);
175        data.modified_stake_pubkeys.insert(pubkey);
176    }
177
178    /// Insert a tombstone for a validator account (marks as deleted).
179    ///
180    /// Also tracks the pubkey as modified for persistence.
181    pub fn tombstone_validator_account(&self, pubkey: Pubkey) {
182        let mut data = self.write();
183        data.validator_accounts.insert(pubkey, None);
184        data.modified_validator_pubkeys.insert(pubkey);
185    }
186
187    /// Get the epoch of this cache.
188    pub fn epoch(&self) -> Epoch {
189        self.read().epoch
190    }
191
192    /// Get the timestamp of this cache.
193    pub fn timestamp(&self) -> u64 {
194        self.read().timestamp
195    }
196
197    /// Set the epoch of this cache.
198    pub fn set_epoch(&self, epoch: Epoch) {
199        self.write().epoch = epoch;
200    }
201
202    /// Set the timestamp of this cache.
203    pub fn set_timestamp(&self, timestamp: u64) {
204        self.write().timestamp = timestamp;
205    }
206
207    /// Check an account and store it in the appropriate cache if it belongs to
208    /// StakeManager or ValidatorRegistry programs.
209    ///
210    /// - If the account has zero kelvins, it is evicted from the cache (tombstoned)
211    /// - If the account is owned by StakeManager, it is stored in stake_accounts
212    /// - If the account is owned by ValidatorRegistry, it is stored in validator_accounts
213    pub fn check_and_update(&self, pubkey: &Pubkey, account: &impl ReadableAccount) {
214        let owner = account.owner();
215
216        // Zero kelvin accounts should be marked as tombstones (None) in the delta
217        if account.kelvins() == 0 {
218            if rialo_stake_manager_interface::check_id(owner) {
219                // Insert tombstone (None) to mark deletion in this epoch's delta
220                self.tombstone_stake_account(*pubkey);
221            } else if rialo_validator_registry_interface::check_id(owner) {
222                // Insert tombstone (None) to mark deletion in this epoch's delta
223                self.tombstone_validator_account(*pubkey);
224            }
225        } else if rialo_stake_manager_interface::check_id(owner) {
226            // Handle StakeManager accounts
227            if let Ok(stake_info) = bincode::deserialize::<StakeInfo>(account.data()) {
228                self.insert_stake_account(
229                    *pubkey,
230                    StakeAccount {
231                        kelvins: account.kelvins(),
232                        data: stake_info,
233                    },
234                );
235            }
236        } else if rialo_validator_registry_interface::check_id(owner) {
237            // Handle ValidatorRegistry accounts
238            if let Ok(validator_info) = bincode::deserialize::<ValidatorInfo>(account.data()) {
239                self.insert_validator_account(
240                    *pubkey,
241                    ValidatorAccount {
242                        kelvins: account.kelvins(),
243                        data: validator_info,
244                    },
245                );
246            }
247        }
248    }
249}
250
251/// Data structure holding the cached stake and validator accounts.
252///
253/// Uses `HashMap<Pubkey, Option<T>>` to support the delta-based persistence model:
254/// - `Some(account)` = account was added or updated
255/// - `None` = account was deleted (tombstone)
256///
257/// In `baseline`, values are always `Some(...)` since it represents complete state.
258/// In `pending` and `frozen` deltas, `None` indicates deletion.
259#[derive(Debug, Default, Clone)]
260pub struct StakeCacheData {
261    /// Map of stake accounts by public key.
262    /// `None` value indicates a tombstone (account was deleted during this epoch).
263    pub stake_accounts: HashMap<Pubkey, Option<StakeAccount>>,
264    /// Map of validator accounts by public key.
265    /// `None` value indicates a tombstone (account was deleted during this epoch).
266    pub validator_accounts: HashMap<Pubkey, Option<ValidatorAccount>>,
267    /// The epoch counter when this snapshot was taken.
268    pub epoch: Epoch,
269    /// The block's Unix timestamp (in milliseconds) when this snapshot was taken.
270    /// This is set when FreezeStakes is called and represents the epoch boundary.
271    pub timestamp: u64,
272    /// Set of stake account pubkeys modified during the current block.
273    /// Used to track which accounts need to be persisted to the deltas CF.
274    /// This is cleared after each `finalize()` call.
275    pub modified_stake_pubkeys: HashSet<Pubkey>,
276    /// Set of validator account pubkeys modified during the current block.
277    /// Used to track which accounts need to be persisted to the deltas CF.
278    /// This is cleared after each `finalize()` call.
279    pub modified_validator_pubkeys: HashSet<Pubkey>,
280}
281
282impl StakeCacheData {
283    /// Drain the modified pubkey sets, returning the pubkeys and clearing the sets.
284    ///
285    /// This is called by `StateStore::finalize()` to get the list of accounts
286    /// that need to be persisted to the deltas CF. After this call, both
287    /// `modified_stake_pubkeys` and `modified_validator_pubkeys` will be empty.
288    ///
289    /// Returns a tuple of `(stake_pubkeys, validator_pubkeys)`.
290    pub fn drain_modified(&mut self) -> (HashSet<Pubkey>, HashSet<Pubkey>) {
291        let stake_pubkeys = std::mem::take(&mut self.modified_stake_pubkeys);
292        let validator_pubkeys = std::mem::take(&mut self.modified_validator_pubkeys);
293        (stake_pubkeys, validator_pubkeys)
294    }
295
296    /// Check if there are any modified accounts pending persistence.
297    pub fn has_modified(&self) -> bool {
298        !self.modified_stake_pubkeys.is_empty() || !self.modified_validator_pubkeys.is_empty()
299    }
300}
301
302/// A history of frozen stake cache snapshots across epochs.
303///
304/// This wraps `VecDeque<StakeCacheData>` in `Arc<RwLock<...>>` to allow thread-safe
305/// shared access. The Arc allows the same history to be shared between the Bank
306/// and StakesHandle.
307///
308/// This maintains a queue of stake snapshots, with the oldest at the front
309/// and the most recent at the back. The ValidatorRegistry builtin pushes
310/// new snapshots, and the Bank pops completed epochs after reward distribution.
311#[derive(Debug, Clone)]
312pub struct StakeHistory(Arc<RwLock<VecDeque<StakeCacheData>>>);
313
314impl Default for StakeHistory {
315    fn default() -> Self {
316        Self(Arc::new(RwLock::new(VecDeque::new())))
317    }
318}
319
320impl StakeHistory {
321    /// Create a new empty stake history.
322    pub fn new() -> Self {
323        Self::default()
324    }
325
326    /// Create a stake history with an initial entry.
327    pub fn with_entry(data: StakeCacheData) -> Self {
328        let mut deque = VecDeque::new();
329        deque.push_back(data);
330        Self(Arc::new(RwLock::new(deque)))
331    }
332
333    /// Create a stake history from an existing Arc (for sharing references).
334    pub fn from_arc(arc: Arc<RwLock<VecDeque<StakeCacheData>>>) -> Self {
335        Self(arc)
336    }
337
338    /// Get a clone of the inner Arc for sharing.
339    pub fn arc_clone(&self) -> Arc<RwLock<VecDeque<StakeCacheData>>> {
340        Arc::clone(&self.0)
341    }
342
343    /// Acquire a read lock on the inner data.
344    pub fn read(&self) -> std::sync::RwLockReadGuard<'_, VecDeque<StakeCacheData>> {
345        self.0.read().expect("Failed to acquire read lock")
346    }
347
348    /// Acquire a write lock on the inner data.
349    pub fn write_lock(&self) -> std::sync::RwLockWriteGuard<'_, VecDeque<StakeCacheData>> {
350        self.0.write().expect("Failed to acquire write lock")
351    }
352
353    /// Push a new snapshot to the back of the history.
354    pub fn push_back(&self, data: StakeCacheData) {
355        self.0
356            .write()
357            .expect("Failed to acquire lock")
358            .push_back(data);
359    }
360
361    /// Pop the oldest snapshot from the front of the history.
362    pub fn pop_front(&self) -> Option<StakeCacheData> {
363        self.0.write().expect("Failed to acquire lock").pop_front()
364    }
365
366    /// Get the number of snapshots in the history.
367    pub fn len(&self) -> usize {
368        self.0.read().expect("Failed to acquire lock").len()
369    }
370
371    /// Check if the history is empty.
372    pub fn is_empty(&self) -> bool {
373        self.0.read().expect("Failed to acquire lock").is_empty()
374    }
375
376    /// Get a clone of the oldest snapshot (front).
377    pub fn front(&self) -> Option<StakeCacheData> {
378        self.0
379            .read()
380            .expect("Failed to acquire lock")
381            .front()
382            .cloned()
383    }
384
385    /// Get a clone of the newest snapshot (back).
386    ///
387    /// This returns the CURRENT epoch's frozen stake data. In normal operation,
388    /// this is never `None` because Bank initialization guarantees at least one
389    /// entry exists after genesis/register_validators.
390    ///
391    /// Use this for lookups that need the current epoch's effective stake state
392    /// (as opposed to `StakesHandle::pending` which is the next epoch being accumulated).
393    pub fn back(&self) -> Option<StakeCacheData> {
394        self.0
395            .read()
396            .expect("Failed to acquire lock")
397            .back()
398            .cloned()
399    }
400
401    /// Iterate over all snapshots from oldest to newest, returning cloned data.
402    ///
403    /// Note: This clones all entries. For large histories, consider accessing
404    /// specific entries via `front()` or `back()` instead.
405    pub fn iter_cloned(&self) -> Vec<StakeCacheData> {
406        self.0
407            .read()
408            .expect("Failed to acquire lock")
409            .iter()
410            .cloned()
411            .collect()
412    }
413}
414
415/// Represents a stake account with its data.
416#[derive(Debug, Clone)]
417pub struct StakeAccount {
418    /// The kelvins balance of the stake account.
419    pub kelvins: u64,
420    /// The deserialized stake info.
421    pub data: StakeInfo,
422}
423
424/// Represents a validator account with its data.
425#[derive(Debug, Clone)]
426pub struct ValidatorAccount {
427    /// The kelvins balance of the validator account.
428    pub kelvins: u64,
429    /// The deserialized validator info.
430    pub data: ValidatorInfo,
431}
432
433/// Handle for builtin programs to access stake cache data and freeze stakes.
434///
435/// This handle provides:
436/// - Read/write access to the pending (next epoch) stake cache data
437/// - Layered lookup across baseline, frozen, and pending
438/// - The ability to freeze the pending stakes into frozen via `freeze_stakes()`
439/// - Callback to check if EpochRewards exists for a given epoch
440///
441/// # Architecture: Baseline + Deltas
442///
443/// The stake cache uses a layered architecture:
444/// - **baseline**: Complete historical state (empty at genesis, populated during EpochRewards activation)
445/// - **frozen**: VecDeque of per-epoch deltas awaiting reward distribution (FIFO order)
446/// - **pending**: Current epoch's changes being accumulated
447///
448/// Lookups search: pending → frozen (newest to oldest) → baseline
449///
450/// # Epoch Semantics
451///
452/// **Important:** The `pending` field contains data for the NEXT epoch (i.e., changes being
453/// accumulated that will take effect after FreezeStakes). To get the CURRENT epoch's frozen
454/// data for lookups, use `frozen.back()` instead.
455///
456/// The handle is cached at block level for performance. Since the handle uses shared
457/// `Arc<RwLock<...>>` references, mutations to pending are immediately visible without
458/// needing to recreate the handle.
459///
460/// # Thread Safety
461///
462/// `StakeCache` and `StakeHistory` wrap their data in `Arc<RwLock<...>>` internally,
463/// allowing safe concurrent access from builtin programs during transaction execution.
464/// Mutations to `pending` are immediately visible to the owning Bank since they share
465/// the same Arc.
466///
467/// # Field Access
468///
469/// The `baseline`, `pending`, and `frozen` fields are private to enforce proper layered lookups.
470/// Use the provided methods for queries:
471/// - `get_stake_account()` - layered lookup for a single stake account
472/// - `get_validator_account()` - layered lookup for a single validator account
473/// - `get_all_validator_accounts()` - merged view of all validators
474/// - `freeze_stakes()` - freeze pending stakes
475/// - `epoch_rewards_exists()` - check if EpochRewards account exists for an epoch
476///
477/// Direct field access is only available via `#[cfg(test)]` accessors for unit tests.
478pub struct StakesHandle {
479    /// Complete state at historical epoch boundary (for fallback lookups).
480    ///
481    /// At genesis, this is empty. After EpochRewards activation, it contains all accounts
482    /// that existed before the oldest epoch still awaiting reward distribution.
483    /// Values are always `Some(...)` in the baseline (no tombstones).
484    baseline: StakeCache,
485
486    /// Stake cache data for the NEXT epoch (pending/accumulating changes).
487    ///
488    /// This is a mutable working copy that accumulates stake and validator account
489    /// modifications throughout the epoch. These changes will become effective after
490    /// the next FreezeStakes call. For current epoch lookups (the frozen effective
491    /// state), use `frozen.back()` instead.
492    ///
493    /// The `StakeCache` wrapper contains `Arc<RwLock<...>>` internally, allowing
494    /// builtin programs to mutate the pending stake data during transaction execution,
495    /// with changes visible to the Bank.
496    pending: StakeCache,
497
498    /// Frozen snapshots for epochs awaiting reward distribution (FIFO order).
499    ///
500    /// Each entry contains ONLY the accounts that changed during that epoch
501    /// (delta, not full state). `Some(account)` = added/updated, `None` = deleted.
502    /// The oldest entry is at the front, the newest at the back.
503    ///
504    /// The `StakeHistory` wrapper contains `Arc<RwLock<...>>` internally.
505    frozen: StakeHistory,
506
507    /// Data for epoch rewards initialization (epoch number and total rewards).
508    /// Set by `request_epoch_rewards_init()`, consumed by `take_epoch_rewards_init_request()`.
509    epoch_rewards_init: Arc<RwLock<Option<EpochRewardsInitRequest>>>,
510
511    /// Signal that FreezeStakes has been called this epoch.
512    /// Set to true by `freeze_stakes()`, consumed by
513    /// `take_epoch_stakes_frozen()` in Bank's `apply_pending_validator_changes_if_needed()`.
514    ///
515    /// This signal is used to trigger the application of pending validator changes
516    /// (e.g., new_commission_rate → commission_rate) at the epoch boundary, even
517    /// when DistributeRewards hasn't run yet.
518    epoch_stakes_frozen: Arc<AtomicBool>,
519
520    /// Callback to check if an EpochRewards account exists for a given epoch.
521    /// Provided by Bank with access to StateStore. Used by DistributeRewards
522    /// to find the first completed frozen epoch without an EpochRewards account.
523    /// Set at construction time, immutable afterwards.
524    epoch_rewards_exists_fn: Arc<dyn Fn(u64) -> bool + Send + Sync>,
525}
526
527/// Request data for epoch rewards initialization.
528/// Used to pass information from DistributeRewards instruction to Bank.
529#[derive(Debug, Clone)]
530pub struct EpochRewardsInitRequest {
531    /// The epoch for which rewards are being distributed.
532    pub epoch: Epoch,
533    /// The total rewards to distribute (hardcoded for MVP).
534    pub total_rewards: u64,
535}
536
537impl std::fmt::Debug for StakesHandle {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        f.debug_struct("StakesHandle")
540            .field("baseline", &self.baseline)
541            .field("pending", &self.pending)
542            .field("frozen", &self.frozen)
543            .field("epoch_rewards_init", &self.epoch_rewards_init)
544            .field("epoch_stakes_frozen", &self.epoch_stakes_frozen)
545            .field("epoch_rewards_exists_fn", &"<callback>")
546            .finish()
547    }
548}
549
550impl Clone for StakesHandle {
551    fn clone(&self) -> Self {
552        Self {
553            baseline: self.baseline.clone(),
554            pending: self.pending.clone(),
555            frozen: self.frozen.clone(),
556            epoch_rewards_init: self.epoch_rewards_init.clone(),
557            epoch_stakes_frozen: Arc::clone(&self.epoch_stakes_frozen),
558            epoch_rewards_exists_fn: Arc::clone(&self.epoch_rewards_exists_fn),
559        }
560    }
561}
562
563impl Default for StakesHandle {
564    fn default() -> Self {
565        Self {
566            baseline: StakeCache::default(),
567            pending: StakeCache::default(),
568            frozen: StakeHistory::default(),
569            epoch_rewards_init: Arc::new(RwLock::new(None)),
570            epoch_stakes_frozen: Arc::new(AtomicBool::new(false)),
571            epoch_rewards_exists_fn: Arc::new(|_| false),
572        }
573    }
574}
575
576impl StakesHandle {
577    /// Create a new stakes handle with shared references.
578    ///
579    /// This shares the same `Arc<RwLock<...>>` with the Bank, so mutations
580    /// to `pending` by builtin programs are immediately visible to the Bank.
581    ///
582    /// The signaling Arcs (`epoch_rewards_init`, `epoch_stakes_frozen`)
583    /// are created internally with default values. This simplifies the API since callers
584    /// don't need to manage these internal signaling mechanisms.
585    ///
586    /// # Arguments
587    /// * `baseline` - The baseline stake cache
588    /// * `pending` - The pending stake cache for the next epoch
589    /// * `frozen` - The frozen stake history
590    /// * `epoch_rewards_exists_fn` - Callback to check if an EpochRewards account exists
591    pub fn new_shared(
592        baseline: StakeCache,
593        pending: StakeCache,
594        frozen: StakeHistory,
595        epoch_rewards_exists_fn: Arc<dyn Fn(u64) -> bool + Send + Sync>,
596    ) -> Self {
597        Self {
598            baseline,
599            pending,
600            frozen,
601            epoch_rewards_init: Arc::new(RwLock::new(None)),
602            epoch_stakes_frozen: Arc::new(AtomicBool::new(false)),
603            epoch_rewards_exists_fn,
604        }
605    }
606
607    /// Check if an EpochRewards account exists for the given epoch.
608    ///
609    /// Uses the callback provided at construction time to query the StateStore.
610    /// This allows DistributeRewards to find the first completed frozen epoch
611    /// that doesn't yet have an EpochRewards account.
612    pub fn epoch_rewards_exists(&self, epoch: u64) -> bool {
613        (self.epoch_rewards_exists_fn)(epoch)
614    }
615
616    /// Signal that epoch stakes have been frozen (FreezeStakes was called).
617    ///
618    /// This sets the `epoch_stakes_frozen` flag to true to signal that
619    /// `apply_pending_validator_changes_if_needed()` should be called by the Bank.
620    pub fn set_epoch_stakes_frozen(&self) {
621        self.epoch_stakes_frozen.store(true, Ordering::Release);
622    }
623
624    /// Atomically take the epoch_stakes_frozen signal.
625    ///
626    /// This atomically reads and clears the flag, returning `true` if it was set.
627    /// Used by `finalize_impl()` to consume the signal and perform the deferred
628    /// pending → frozen swap.
629    ///
630    /// Returns `true` if FreezeStakes was called and the signal hadn't been consumed yet.
631    pub fn take_epoch_stakes_frozen(&self) -> bool {
632        self.epoch_stakes_frozen.swap(false, Ordering::AcqRel)
633    }
634
635    /// Check if FreezeStakes was signaled this block, without consuming the flag.
636    ///
637    /// Used by `apply_pending_validator_changes_if_needed()` to detect the epoch
638    /// boundary while leaving the flag set for `finalize_impl()` to consume.
639    pub fn is_epoch_stakes_frozen(&self) -> bool {
640        self.epoch_stakes_frozen.load(Ordering::Acquire)
641    }
642
643    // ========== Layered Lookups from Pending ==========
644    // These methods include pending changes (next epoch) in the lookup.
645
646    /// Get a stake account starting from pending (next epoch state).
647    ///
648    /// Searches: pending → frozen (newest to oldest) → baseline
649    ///
650    /// Returns `Some(account)` if found, `None` if the account doesn't exist
651    /// (either never created or was deleted via tombstone).
652    pub fn get_stake_account_from_pending(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
653        // 1. Check pending (next epoch)
654        {
655            let pending_data = self.pending.read();
656            if let Some(value) = pending_data.stake_accounts.get(pubkey) {
657                return value.clone(); // Some(account) or None (tombstone)
658            }
659        }
660
661        // 2. Check frozen epochs in reverse order (newest to oldest)
662        {
663            let frozen_data = self.frozen.read();
664            for frozen_entry in frozen_data.iter().rev() {
665                if let Some(value) = frozen_entry.stake_accounts.get(pubkey) {
666                    return value.clone();
667                }
668            }
669        }
670
671        // 3. Check baseline
672        {
673            let baseline_data = self.baseline.read();
674            baseline_data
675                .stake_accounts
676                .get(pubkey)
677                .and_then(|v| v.clone())
678        }
679    }
680
681    /// Get a validator account starting from pending (next epoch state).
682    ///
683    /// Searches: pending → frozen (newest to oldest) → baseline
684    ///
685    /// Returns `Some(account)` if found, `None` if the account doesn't exist
686    /// (either never created or was deleted via tombstone).
687    pub fn get_validator_account_from_pending(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
688        // 1. Check pending (next epoch)
689        {
690            let pending_data = self.pending.read();
691            if let Some(value) = pending_data.validator_accounts.get(pubkey) {
692                return value.clone(); // Some(account) or None (tombstone)
693            }
694        }
695
696        // 2. Check frozen epochs in reverse order (newest to oldest)
697        {
698            let frozen_data = self.frozen.read();
699            for frozen_entry in frozen_data.iter().rev() {
700                if let Some(value) = frozen_entry.validator_accounts.get(pubkey) {
701                    return value.clone();
702                }
703            }
704        }
705
706        // 3. Check baseline
707        {
708            let baseline_data = self.baseline.read();
709            baseline_data
710                .validator_accounts
711                .get(pubkey)
712                .and_then(|v| v.clone())
713        }
714    }
715
716    /// Get all validator accounts starting from pending (next epoch state).
717    ///
718    /// Returns a vector of `(pubkey, account)` pairs for all validators, sorted by pubkey.
719    /// Includes pending changes (next epoch).
720    /// Note: This is O(baseline_size + total_deltas).
721    pub fn get_all_validator_accounts_from_pending(&self) -> Vec<(Pubkey, ValidatorAccount)> {
722        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
723
724        // 1. Start with baseline
725        {
726            let baseline_data = self.baseline.read();
727            for (pubkey, value) in baseline_data.validator_accounts.iter() {
728                result.insert(*pubkey, value.clone());
729            }
730        }
731
732        // 2. Apply frozen deltas in order (oldest to newest)
733        {
734            let frozen_data = self.frozen.read();
735            for frozen_entry in frozen_data.iter() {
736                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
737                    result.insert(*pubkey, value.clone());
738                }
739            }
740        }
741
742        // 3. Apply pending deltas
743        {
744            let pending_data = self.pending.read();
745            for (pubkey, value) in pending_data.validator_accounts.iter() {
746                result.insert(*pubkey, value.clone());
747            }
748        }
749
750        // 4. Filter out tombstones and collect
751        let mut sorted: Vec<_> = result
752            .into_iter()
753            .filter_map(|(k, v)| v.map(|account| (k, account)))
754            .collect();
755
756        // Sort by pubkey for deterministic ordering
757        sorted.sort_by_key(|(pubkey, _)| *pubkey);
758        sorted
759    }
760
761    // ========== Layered Lookups from Last Frozen ==========
762    // These methods represent the current epoch's effective state (skip pending).
763
764    /// Get a stake account starting from the last frozen epoch (current epoch state).
765    ///
766    /// Searches: frozen (newest to oldest) → baseline
767    /// Skips pending (next epoch changes).
768    ///
769    /// Returns `Some(account)` if found, `None` if the account doesn't exist
770    /// (either never created or was deleted via tombstone).
771    pub fn get_stake_account_from_last_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
772        // 1. Check frozen epochs in reverse order (newest to oldest)
773        {
774            let frozen_data = self.frozen.read();
775            for frozen_entry in frozen_data.iter().rev() {
776                if let Some(value) = frozen_entry.stake_accounts.get(pubkey) {
777                    return value.clone();
778                }
779            }
780        }
781
782        // 2. Check baseline
783        {
784            let baseline_data = self.baseline.read();
785            baseline_data
786                .stake_accounts
787                .get(pubkey)
788                .and_then(|v| v.clone())
789        }
790    }
791
792    /// Get a validator account starting from the last frozen epoch (current epoch state).
793    ///
794    /// Searches: frozen (newest to oldest) → baseline
795    /// Skips pending (next epoch changes).
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_validator_account_from_last_frozen(
800        &self,
801        pubkey: &Pubkey,
802    ) -> Option<ValidatorAccount> {
803        // 1. Check frozen epochs in reverse order (newest to oldest)
804        {
805            let frozen_data = self.frozen.read();
806            for frozen_entry in frozen_data.iter().rev() {
807                if let Some(value) = frozen_entry.validator_accounts.get(pubkey) {
808                    return value.clone();
809                }
810            }
811        }
812
813        // 2. Check baseline
814        {
815            let baseline_data = self.baseline.read();
816            baseline_data
817                .validator_accounts
818                .get(pubkey)
819                .and_then(|v| v.clone())
820        }
821    }
822
823    /// Get all validator accounts from the last frozen epoch (current epoch state).
824    ///
825    /// Returns a vector of `(pubkey, account)` pairs for all validators, sorted by pubkey.
826    /// Skips pending (next epoch changes).
827    /// Note: This is O(baseline_size + total_frozen_deltas).
828    pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
829        let mut result: HashMap<Pubkey, Option<ValidatorAccount>> = HashMap::new();
830
831        // 1. Start with baseline
832        {
833            let baseline_data = self.baseline.read();
834            for (pubkey, value) in baseline_data.validator_accounts.iter() {
835                result.insert(*pubkey, value.clone());
836            }
837        }
838
839        // 2. Apply all frozen deltas in order (oldest to newest)
840        {
841            let frozen_data = self.frozen.read();
842            for frozen_entry in frozen_data.iter() {
843                for (pubkey, value) in frozen_entry.validator_accounts.iter() {
844                    result.insert(*pubkey, value.clone());
845                }
846            }
847        }
848
849        // 3. Filter out tombstones and collect (skip pending)
850        let mut sorted: Vec<_> = result
851            .into_iter()
852            .filter_map(|(k, v)| v.map(|account| (k, account)))
853            .collect();
854
855        // Sort by pubkey for deterministic ordering
856        sorted.sort_by_key(|(pubkey, _)| *pubkey);
857        sorted
858    }
859
860    // ========== Layered Lookups from First Frozen ==========
861    // These methods represent the oldest pending rewards epoch state.
862
863    /// Get a stake account starting from the first frozen epoch (oldest pending rewards).
864    ///
865    /// Searches: frozen.front() → baseline only
866    /// Skips all newer frozen epochs and pending.
867    ///
868    /// Returns `Some(account)` if found, `None` if the account doesn't exist
869    /// (either never created or was deleted via tombstone).
870    pub fn get_stake_account_from_first_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
871        // 1. Check first frozen epoch only
872        {
873            let frozen_data = self.frozen.read();
874            if let Some(first_frozen) = frozen_data.front() {
875                if let Some(value) = first_frozen.stake_accounts.get(pubkey) {
876                    return value.clone();
877                }
878            }
879        }
880
881        // 2. Check baseline
882        {
883            let baseline_data = self.baseline.read();
884            baseline_data
885                .stake_accounts
886                .get(pubkey)
887                .and_then(|v| v.clone())
888        }
889    }
890
891    /// Get all stake accounts starting from pending (next epoch state).
892    ///
893    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts, sorted by pubkey.
894    /// Includes pending changes (next epoch).
895    /// Note: This is O(baseline_size + total_deltas).
896    ///
897    /// This method is used for operations that need to check all stake accounts
898    /// including the most recent changes (e.g., checking validator references during Withdraw).
899    pub fn get_all_stake_accounts_from_pending(&self) -> Vec<(Pubkey, StakeAccount)> {
900        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
901
902        // 1. Start with baseline
903        {
904            let baseline_data = self.baseline.read();
905            for (pubkey, value) in baseline_data.stake_accounts.iter() {
906                result.insert(*pubkey, value.clone());
907            }
908        }
909
910        // 2. Apply frozen deltas in order (oldest to newest)
911        {
912            let frozen_data = self.frozen.read();
913            for frozen_entry in frozen_data.iter() {
914                for (pubkey, value) in frozen_entry.stake_accounts.iter() {
915                    result.insert(*pubkey, value.clone());
916                }
917            }
918        }
919
920        // 3. Apply pending deltas
921        {
922            let pending_data = self.pending.read();
923            for (pubkey, value) in pending_data.stake_accounts.iter() {
924                result.insert(*pubkey, value.clone());
925            }
926        }
927
928        // 4. Filter out tombstones and collect
929        let mut sorted: Vec<_> = result
930            .into_iter()
931            .filter_map(|(k, v)| v.map(|account| (k, account)))
932            .collect();
933
934        // Sort by pubkey for deterministic ordering
935        sorted.sort_by_key(|(pubkey, _)| *pubkey);
936        sorted
937    }
938
939    /// Get all stake accounts from the first frozen epoch (oldest pending rewards).
940    ///
941    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts, sorted by pubkey.
942    /// Skips all newer frozen epochs and pending.
943    ///
944    /// This method is used by reward calculation to iterate over all stake accounts
945    /// that were active at the time rewards were frozen (baseline + first frozen delta).
946    pub fn get_all_stake_accounts_from_first_frozen(&self) -> Vec<(Pubkey, StakeAccount)> {
947        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
948
949        // 1. Start with baseline
950        {
951            let baseline_data = self.baseline.read();
952            for (pubkey, value) in baseline_data.stake_accounts.iter() {
953                result.insert(*pubkey, value.clone());
954            }
955        }
956
957        // 2. Apply only the first frozen delta
958        {
959            let frozen_data = self.frozen.read();
960            if let Some(first_frozen) = frozen_data.front() {
961                for (pubkey, value) in first_frozen.stake_accounts.iter() {
962                    result.insert(*pubkey, value.clone());
963                }
964            }
965        }
966
967        // 3. Filter out tombstones and collect
968        let mut sorted: Vec<_> = result
969            .into_iter()
970            .filter_map(|(k, v)| v.map(|account| (k, account)))
971            .collect();
972
973        // Sort by pubkey for deterministic ordering
974        sorted.sort_by_key(|(pubkey, _)| *pubkey);
975        sorted
976    }
977
978    /// Get all stake accounts from baseline + frozen deltas up to (and including)
979    /// the specified epoch.
980    ///
981    /// Lookups: baseline + frozen deltas where `delta.epoch <= target_epoch`
982    pub fn get_all_stake_accounts_from_frozen_epoch(
983        &self,
984        target_epoch: Epoch,
985    ) -> Vec<(Pubkey, StakeAccount)> {
986        let mut result: HashMap<Pubkey, Option<StakeAccount>> = HashMap::new();
987
988        // 1. Start with baseline
989        {
990            let baseline_data = self.baseline.read();
991            for (pubkey, value) in baseline_data.stake_accounts.iter() {
992                result.insert(*pubkey, value.clone());
993            }
994        }
995
996        // 2. Apply frozen deltas up to and including target_epoch
997        {
998            let frozen_data = self.frozen.read();
999            for frozen_entry in frozen_data.iter() {
1000                if frozen_entry.epoch > target_epoch {
1001                    break; // Frozen is ordered oldest-to-newest, stop at target
1002                }
1003                for (pubkey, value) in frozen_entry.stake_accounts.iter() {
1004                    result.insert(*pubkey, value.clone());
1005                }
1006            }
1007        }
1008
1009        // 3. Filter out tombstones and collect
1010        let mut sorted: Vec<_> = result
1011            .into_iter()
1012            .filter_map(|(k, v)| v.map(|account| (k, account)))
1013            .collect();
1014
1015        // Sort by pubkey for deterministic ordering
1016        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1017        sorted
1018    }
1019
1020    // ========== Baseline-Only Lookups ==========
1021    // These methods return data from the baseline only (after merge has happened).
1022    // Used by the new reward calculation model where merge happens at activation.
1023
1024    /// Get all stake accounts from the baseline only.
1025    ///
1026    /// Returns a vector of `(pubkey, account)` pairs for all stake accounts in baseline,
1027    /// sorted by pubkey. Does NOT include frozen or pending data.
1028    ///
1029    /// This is used after the frozen.front() has been merged into baseline,
1030    /// for baseline-based reward calculation. The baseline contains the complete
1031    /// state of the epoch being rewarded after merge.
1032    pub fn get_all_stake_accounts_from_baseline(&self) -> Vec<(Pubkey, StakeAccount)> {
1033        let baseline_data = self.baseline.read();
1034        let mut sorted: Vec<_> = baseline_data
1035            .stake_accounts
1036            .iter()
1037            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
1038            .collect();
1039
1040        // Sort by pubkey for deterministic ordering
1041        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1042        sorted
1043    }
1044
1045    /// Get all validator accounts from the baseline only.
1046    ///
1047    /// Returns a vector of `(pubkey, account)` pairs for all validators in baseline,
1048    /// sorted by pubkey. Does NOT include frozen or pending data.
1049    ///
1050    /// This is used after the frozen.front() has been merged into baseline,
1051    /// for baseline-based reward calculation.
1052    pub fn get_all_validator_accounts_from_baseline(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1053        let baseline_data = self.baseline.read();
1054        let mut sorted: Vec<_> = baseline_data
1055            .validator_accounts
1056            .iter()
1057            .filter_map(|(k, v)| v.as_ref().map(|account| (*k, account.clone())))
1058            .collect();
1059
1060        // Sort by pubkey for deterministic ordering
1061        sorted.sort_by_key(|(pubkey, _)| *pubkey);
1062        sorted
1063    }
1064
1065    /// Freeze the pending stake cache data.
1066    ///
1067    /// This performs an O(1) swap of the pending stake cache data using `std::mem::take()`
1068    /// and pushes it to the back of the frozen queue. This is typically called by the
1069    /// ValidatorRegistry program's FreezeStakes instruction to capture the validator set
1070    /// at a specific point.
1071    ///
1072    /// **Note:** Since the handle uses shared `Arc<RwLock<...>>` references, the frozen
1073    /// data and updated pending epoch are immediately visible to all handle instances.
1074    ///
1075    /// To access the frozen validator data after calling this method, use
1076    /// `get_all_validator_accounts_from_last_frozen()`.
1077    pub fn freeze_stakes(&self) {
1078        // 1. Atomically swap pending data with empty and initialize new pending
1079        // Using a single lock scope eliminates any race condition window
1080        let frozen_data = {
1081            let mut pending_guard = self.pending.write();
1082            let frozen_data = std::mem::take(&mut *pending_guard);
1083            // Initialize new pending's epoch and timestamp for next epoch
1084            // This ensures any stake changes after FreezeStakes within the same block
1085            // have the correct epoch/timestamp (not the Default values of 0)
1086            pending_guard.epoch = frozen_data.epoch + 1;
1087            pending_guard.timestamp = frozen_data.timestamp;
1088            frozen_data
1089        };
1090
1091        // 2. Push frozen data to history
1092        self.frozen.push_back(frozen_data);
1093
1094        // 3. Signal that FreezeStakes was called (for apply_pending_validator_changes)
1095        self.epoch_stakes_frozen.store(true, Ordering::Release);
1096    }
1097
1098    // ========== Epoch and Timestamp Accessors ==========
1099
1100    /// Get the epoch of the pending (next) stake cache.
1101    pub fn pending_epoch(&self) -> Epoch {
1102        self.pending.epoch()
1103    }
1104
1105    /// Set the epoch of the pending stake cache.
1106    pub fn set_pending_epoch(&self, epoch: Epoch) {
1107        self.pending.set_epoch(epoch);
1108    }
1109
1110    /// Set the timestamp of the pending stake cache.
1111    pub fn set_pending_timestamp(&self, timestamp: u64) {
1112        self.pending.set_timestamp(timestamp);
1113    }
1114
1115    /// Get the timestamp of the last frozen epoch (current epoch's effective state).
1116    ///
1117    /// Returns `None` if no frozen snapshots exist yet.
1118    pub fn last_frozen_timestamp(&self) -> Option<u64> {
1119        self.frozen.read().back().map(|data| data.timestamp)
1120    }
1121
1122    /// Push a new frozen snapshot to the history.
1123    pub fn push_frozen(&self, data: StakeCacheData) {
1124        self.frozen.push_back(data);
1125    }
1126
1127    /// Get the number of frozen snapshots in the history.
1128    pub fn frozen_len(&self) -> usize {
1129        self.frozen.len()
1130    }
1131
1132    /// Get the epoch of the oldest frozen snapshot (front of the queue).
1133    ///
1134    /// Returns `None` if no frozen snapshots exist.
1135    pub fn front_frozen_epoch(&self) -> Option<Epoch> {
1136        self.frozen.front().map(|data| data.epoch)
1137    }
1138
1139    // ========== Epoch Rewards Signaling ==========
1140
1141    /// Request epoch rewards initialization.
1142    ///
1143    /// This is called by the DistributeRewards instruction to signal that the Bank
1144    /// should create an EpochRewards account. The Bank checks for the request after
1145    /// transaction execution via `take_epoch_rewards_init_request()`.
1146    ///
1147    /// # Arguments
1148    /// * `epoch` - The epoch for which rewards are being distributed
1149    /// * `total_rewards` - The total rewards to distribute (hardcoded for MVP)
1150    pub fn request_epoch_rewards_init(&self, epoch: Epoch, total_rewards: u64) {
1151        // Store the request data - the presence of Some indicates a request is pending
1152        *self
1153            .epoch_rewards_init
1154            .write()
1155            .expect("Failed to acquire lock") = Some(EpochRewardsInitRequest {
1156            epoch,
1157            total_rewards,
1158        });
1159    }
1160
1161    /// Take the epoch rewards initialization request, clearing it.
1162    ///
1163    /// This is called by the Bank after transaction execution to check if epoch
1164    /// rewards init was requested. The Bank uses the returned data to create
1165    /// the EpochRewards account.
1166    ///
1167    /// Returns `Some(request)` if a request was pending, `None` otherwise.
1168    /// After this call, `epoch_rewards_init` will be `None`.
1169    pub fn take_epoch_rewards_init_request(&self) -> Option<EpochRewardsInitRequest> {
1170        // Take and return the request data
1171        self.epoch_rewards_init
1172            .write()
1173            .expect("Failed to acquire lock")
1174            .take()
1175    }
1176
1177    /// Check if an epoch rewards initialization request is pending.
1178    ///
1179    /// This is used by DistributeRewards to fail if a signal is already set
1180    /// for the current block (prevents multiple DistributeRewards in same block).
1181    ///
1182    /// Returns `true` if a request is pending, `false` otherwise.
1183    /// Does NOT consume the request (unlike `take_epoch_rewards_init_request`).
1184    pub fn is_epoch_rewards_init_pending(&self) -> bool {
1185        self.epoch_rewards_init
1186            .read()
1187            .expect("Failed to acquire lock")
1188            .is_some()
1189    }
1190
1191    /// Get completed frozen epochs (excludes the last/current epoch).
1192    ///
1193    /// Returns epoch numbers for all frozen entries except the last one,
1194    /// which represents the currently ongoing epoch. These are epochs
1195    /// that have completed and are eligible for reward distribution.
1196    ///
1197    /// Returns empty if frozen has 0 or 1 entries (need at least 2 to have completed epochs).
1198    pub fn completed_frozen_epochs(&self) -> Vec<Epoch> {
1199        let frozen_data = self.frozen.read();
1200        let len = frozen_data.len();
1201        if len < 2 {
1202            return vec![];
1203        }
1204        frozen_data
1205            .iter()
1206            .take(len - 1) // Exclude last (current epoch)
1207            .map(|data| data.epoch)
1208            .collect()
1209    }
1210
1211    // ========== Validator Reference Checking ==========
1212
1213    /// Check if any stake account references the given validator pubkey whose
1214    /// unbonding period is NOT yet complete.
1215    ///
1216    /// This performs an O(n) search over all stake accounts starting from
1217    /// pending → frozen → baseline. Uses Rayon's parallel iterator for better
1218    /// performance on multi-core systems.
1219    ///
1220    /// A stake account is considered to "reference" the validator if:
1221    /// - It has `validator == Some(target_validator)`, AND
1222    /// - Either:
1223    ///   - It is **active** (no `deactivation_requested`), OR
1224    ///   - It is **still unbonding** (unbonding conditions not yet met)
1225    ///
1226    /// Stake accounts whose unbonding is complete are NOT considered as referencing
1227    /// the validator, since they can be fully withdrawn or reactivated to another validator.
1228    ///
1229    /// # Unbonding Completion Conditions
1230    ///
1231    /// Unbonding is complete when BOTH conditions are met:
1232    /// 1. **State transition**: `deactivation_timestamp < last_freeze_timestamp`
1233    ///    (at least one FreezeStakes has occurred since deactivation)
1234    /// 2. **Duration enforcement**: `deactivation_timestamp + unbonding_period < current_timestamp`
1235    ///    (the unbonding period has actually elapsed)
1236    ///
1237    /// # Arguments
1238    /// * `validator` - The validator pubkey to check
1239    /// * `validator_info` - The validator's info (used to compute unbonding end via `end_of_unbonding`)
1240    /// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
1241    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1242    ///
1243    /// # Returns
1244    /// `true` if at least one stake account references the validator and is either
1245    /// active or still unbonding, `false` otherwise.
1246    ///
1247    /// # Performance
1248    ///
1249    /// This is an expensive O(n) operation that should only be called when needed
1250    /// (e.g., during Withdraw when checking if a validator can be fully drained).
1251    pub fn is_validator_referenced(
1252        &self,
1253        validator: &Pubkey,
1254        validator_info: &ValidatorInfo,
1255        last_freeze_timestamp: u64,
1256        current_timestamp: u64,
1257    ) -> bool {
1258        // Get all stake accounts and check if any reference the validator using parallel iteration
1259        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1260        all_stake_accounts.par_iter().any(|(_, stake_account)| {
1261            // First check: does this stake reference our target validator?
1262            if stake_account.data.validator.as_ref() != Some(validator) {
1263                return false;
1264            }
1265
1266            // If not deactivating (active stake), it counts as referencing
1267            let Some(deactivation_timestamp) = stake_account.data.deactivation_requested else {
1268                return true;
1269            };
1270
1271            // Check if unbonding is complete using the two-step validation:
1272            // 1. State transition: deactivation must have taken effect
1273            if deactivation_timestamp >= last_freeze_timestamp {
1274                // Still deactivating, counts as referencing
1275                return true;
1276            }
1277
1278            // 2. Duration enforcement: unbonding period must have elapsed
1279            let unbonding_end = validator_info.end_of_unbonding(deactivation_timestamp);
1280
1281            // If unbonding is NOT complete, the stake still counts as referencing
1282            unbonding_end >= current_timestamp
1283        })
1284    }
1285
1286    // ========== Locked Staker Checking ==========
1287
1288    /// Check if any stake account delegated to the given validator is still within
1289    /// its lockup period.
1290    ///
1291    /// This performs an O(n) search over all stake accounts starting from
1292    /// pending → frozen → baseline. Uses Rayon's parallel iterator for better
1293    /// performance on multi-core systems.
1294    ///
1295    /// A staker is considered "locked" if ALL of the following are true:
1296    /// - It has `validator == Some(target_validator)` (delegated to this validator)
1297    /// - It has `activation_requested == Some(timestamp)` (was activated)
1298    /// - `activation_requested + lockup_period > current_timestamp` (lockup hasn't expired)
1299    ///
1300    /// Self-bonds are excluded from lockup checks to prevent the validator from being
1301    /// unable to change commission rates or shut down when only the self-bond exists.
1302    ///
1303    /// # Arguments
1304    /// * `validator` - The validator pubkey to check
1305    /// * `lockup_period` - The validator's lockup period in milliseconds
1306    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1307    ///
1308    /// # Returns
1309    /// `true` if at least one stake account is delegated to the validator and still
1310    /// within its lockup period, `false` otherwise.
1311    pub fn has_locked_stakers(
1312        &self,
1313        validator: &Pubkey,
1314        lockup_period: u64,
1315        current_timestamp: u64,
1316    ) -> bool {
1317        let self_bond_pubkey = derive_self_bond_address(validator);
1318        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1319        all_stake_accounts
1320            .par_iter()
1321            .any(|(pubkey, stake_account)| {
1322                // Skip self-bond PDA
1323                if *pubkey == self_bond_pubkey {
1324                    return false;
1325                }
1326
1327                // First check: does this stake reference our target validator?
1328                if stake_account.data.validator.as_ref() != Some(validator) {
1329                    return false;
1330                }
1331
1332                // Must have been activated to have a lockup
1333                let Some(activation_requested) = stake_account.data.activation_requested else {
1334                    return false;
1335                };
1336
1337                // Check if the lockup period hasn't expired yet
1338                let lockup_end = activation_requested.saturating_add(lockup_period);
1339                lockup_end > current_timestamp
1340            })
1341    }
1342
1343    /// Check if a validator is referenced by any stake accounts (excluding the self-bond).
1344    ///
1345    /// This variant excludes the self-bond PDA from the check to prevent circular logic
1346    /// where the self-bond cannot be deactivated because its existence always makes
1347    /// is_validator_referenced() return true.
1348    ///
1349    /// # Arguments
1350    /// * `validator_pubkey` - The validator pubkey to check
1351    /// * `validator_info` - The validator's info (used to compute unbonding end via `end_of_unbonding`)
1352    /// * `last_freeze_timestamp` - When FreezeStakes was last called (epoch boundary)
1353    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
1354    ///
1355    /// # Returns
1356    /// `true` if at least one non-self-bond stake account references the validator
1357    pub fn is_validator_referenced_excluding_self_bond(
1358        &self,
1359        validator_pubkey: &Pubkey,
1360        validator_info: &ValidatorInfo,
1361        last_freeze_timestamp: u64,
1362        current_timestamp: u64,
1363    ) -> bool {
1364        let self_bond_pubkey = derive_self_bond_address(validator_pubkey);
1365        let all_stake_accounts = self.get_all_stake_accounts_from_pending();
1366        all_stake_accounts
1367            .par_iter()
1368            .any(|(pubkey, stake_account)| {
1369                // Skip self-bond PDA
1370                if *pubkey == self_bond_pubkey {
1371                    return false;
1372                }
1373
1374                // First check: does this stake reference our target validator?
1375                if stake_account.data.validator.as_ref() != Some(validator_pubkey) {
1376                    return false;
1377                }
1378
1379                // If not deactivating (active stake), it counts as referencing
1380                let Some(deactivation_timestamp) = stake_account.data.deactivation_requested else {
1381                    return true;
1382                };
1383
1384                // Check if unbonding is complete using the two-step validation:
1385                // 1. State transition: deactivation must have taken effect
1386                if deactivation_timestamp >= last_freeze_timestamp {
1387                    // Still deactivating, counts as referencing
1388                    return true;
1389                }
1390
1391                // 2. Duration enforcement: unbonding period must have elapsed
1392                let unbonding_end = validator_info.end_of_unbonding(deactivation_timestamp);
1393
1394                // If unbonding is NOT complete, the stake still counts as referencing
1395                unbonding_end >= current_timestamp
1396            })
1397    }
1398
1399    // ========== Pending Cache Mutation Accessors ==========
1400
1401    /// Insert a stake account into the pending cache.
1402    pub fn insert_stake_account(&self, pubkey: Pubkey, account: StakeAccount) {
1403        self.pending.insert_stake_account(pubkey, account);
1404    }
1405
1406    /// Insert a validator account into the pending cache.
1407    pub fn insert_validator_account(&self, pubkey: Pubkey, account: ValidatorAccount) {
1408        self.pending.insert_validator_account(pubkey, account);
1409    }
1410}
1411
1412// ========== Read-Only View ==========
1413
1414/// Read-only view of the stake cache for external consumers (e.g., RPC handlers).
1415///
1416/// This type wraps a `StakesHandle` and exposes only read-only query methods.
1417/// Mutation methods (`insert_stake_account`, `insert_validator_account`, `freeze_stakes`,
1418/// `request_epoch_rewards_init`, etc.) are intentionally not exposed.
1419///
1420/// # Usage
1421///
1422/// External code (outside the `svm-execution` crate) should use `Bank::stakes_view()`
1423/// to obtain a `StakesView` instead of accessing the full `StakesHandle` directly.
1424/// This prevents accidental state corruption from RPC handlers or other non-transaction
1425/// code paths.
1426pub struct StakesView(StakesHandle);
1427
1428impl StakesView {
1429    /// Create a new read-only view from a `StakesHandle`.
1430    pub fn new(handle: StakesHandle) -> Self {
1431        Self(handle)
1432    }
1433
1434    // ========== Layered Lookups from Pending ==========
1435
1436    /// Get a stake account starting from pending (next epoch state).
1437    ///
1438    /// Searches: pending → frozen (newest to oldest) → baseline
1439    pub fn get_stake_account_from_pending(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
1440        self.0.get_stake_account_from_pending(pubkey)
1441    }
1442
1443    /// Get a validator account starting from pending (next epoch state).
1444    ///
1445    /// Searches: pending → frozen (newest to oldest) → baseline
1446    pub fn get_validator_account_from_pending(&self, pubkey: &Pubkey) -> Option<ValidatorAccount> {
1447        self.0.get_validator_account_from_pending(pubkey)
1448    }
1449
1450    /// Get all validator accounts starting from pending (next epoch state).
1451    pub fn get_all_validator_accounts_from_pending(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1452        self.0.get_all_validator_accounts_from_pending()
1453    }
1454
1455    // ========== Layered Lookups from Last Frozen ==========
1456
1457    /// Get a stake account starting from the last frozen epoch (current epoch state).
1458    ///
1459    /// Searches: frozen (newest to oldest) → baseline. Skips pending.
1460    pub fn get_stake_account_from_last_frozen(&self, pubkey: &Pubkey) -> Option<StakeAccount> {
1461        self.0.get_stake_account_from_last_frozen(pubkey)
1462    }
1463
1464    /// Get a validator account starting from the last frozen epoch (current epoch state).
1465    ///
1466    /// Searches: frozen (newest to oldest) → baseline. Skips pending.
1467    pub fn get_validator_account_from_last_frozen(
1468        &self,
1469        pubkey: &Pubkey,
1470    ) -> Option<ValidatorAccount> {
1471        self.0.get_validator_account_from_last_frozen(pubkey)
1472    }
1473
1474    /// Get all validator accounts from the last frozen epoch (current epoch state).
1475    pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
1476        self.0.get_all_validator_accounts_from_last_frozen()
1477    }
1478
1479    // ========== Timestamp Accessors ==========
1480
1481    /// Get the timestamp of the last frozen epoch (current epoch's effective state).
1482    ///
1483    /// Returns `None` if no frozen snapshots exist yet.
1484    pub fn last_frozen_timestamp(&self) -> Option<u64> {
1485        self.0.last_frozen_timestamp()
1486    }
1487}
1488
1489// ========== Test-only accessors ==========
1490#[cfg(test)]
1491impl StakesHandle {
1492    /// Get direct access to baseline for test assertions.
1493    pub fn raw_baseline(&self) -> &StakeCache {
1494        &self.baseline
1495    }
1496
1497    /// Get direct access to pending for test assertions.
1498    pub fn raw_pending(&self) -> &StakeCache {
1499        &self.pending
1500    }
1501
1502    /// Get direct access to frozen for test assertions.
1503    pub fn raw_frozen(&self) -> &StakeHistory {
1504        &self.frozen
1505    }
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510    use rialo_stake_manager_interface::instruction::StakeInfo;
1511    use rialo_validator_registry_interface::instruction::ValidatorInfo;
1512
1513    use super::*;
1514
1515    // ========================================================================
1516    // Test Helper Functions
1517    // ========================================================================
1518
1519    fn create_test_stake_account(kelvins: u64, validator: Pubkey) -> StakeAccount {
1520        StakeAccount {
1521            kelvins,
1522            data: StakeInfo {
1523                activation_requested: Some(0),
1524                deactivation_requested: None,
1525                delegated_balance: kelvins,
1526                validator: Some(validator),
1527                admin_authority: Pubkey::new_unique(),
1528                withdraw_authority: Pubkey::new_unique(),
1529                reward_receiver: None,
1530            },
1531        }
1532    }
1533
1534    fn create_test_validator_account(kelvins: u64, stake: u64) -> ValidatorAccount {
1535        ValidatorAccount {
1536            kelvins,
1537            data: ValidatorInfo {
1538                node_identity: Pubkey::new_unique(),
1539                authorized_withdrawer: Pubkey::new_unique(),
1540                registration_time: 0,
1541                stake,
1542                address: vec![],
1543                hostname: String::new(),
1544                authority_key: vec![0u8; 96],
1545                protocol_key: Pubkey::new_unique(),
1546                network_key: Pubkey::new_unique(),
1547                last_update: 0,
1548                unbonding_periods: std::collections::BTreeMap::from([(0, 0)]),
1549                lockup_period: 0,
1550                commission_rate: 500,
1551                new_commission_rate: None,
1552                earliest_shutdown: None,
1553            },
1554        }
1555    }
1556
1557    // ========================================================================
1558    // Layered Lookup Tests: pending → frozen → baseline
1559    // ========================================================================
1560
1561    #[test]
1562    fn test_layered_lookup_stake_account_from_pending() {
1563        let pubkey = Pubkey::new_unique();
1564        let validator = Pubkey::new_unique();
1565        let handle = StakesHandle::default();
1566
1567        // Insert into pending
1568        let pending_account = create_test_stake_account(1000, validator);
1569        handle.insert_stake_account(pubkey, pending_account.clone());
1570
1571        // Lookup should find in pending
1572        let found = handle.get_stake_account_from_pending(&pubkey);
1573        assert!(found.is_some());
1574        assert_eq!(found.unwrap().kelvins, 1000);
1575    }
1576
1577    #[test]
1578    fn test_layered_lookup_stake_account_from_frozen() {
1579        let pubkey = Pubkey::new_unique();
1580        let validator = Pubkey::new_unique();
1581        let handle = StakesHandle::default();
1582
1583        // Insert into pending and freeze
1584        let account = create_test_stake_account(2000, validator);
1585        handle.insert_stake_account(pubkey, account);
1586        handle.freeze_stakes();
1587
1588        // Account should now be in frozen, pending should be empty
1589        let found = handle.get_stake_account_from_pending(&pubkey);
1590        assert!(found.is_some());
1591        assert_eq!(found.unwrap().kelvins, 2000);
1592
1593        // Confirm pending is empty
1594        assert!(handle.raw_pending().get_stake_account(&pubkey).is_none());
1595    }
1596
1597    #[test]
1598    fn test_layered_lookup_stake_account_from_baseline() {
1599        let pubkey = Pubkey::new_unique();
1600        let validator = Pubkey::new_unique();
1601
1602        // Create a handle with account in baseline
1603        let mut baseline_data = StakeCacheData::default();
1604        baseline_data
1605            .stake_accounts
1606            .insert(pubkey, Some(create_test_stake_account(3000, validator)));
1607        let baseline = StakeCache::with_data(baseline_data);
1608        let handle = StakesHandle::new_shared(
1609            baseline,
1610            StakeCache::default(),
1611            StakeHistory::default(),
1612            Arc::new(|_| false),
1613        );
1614
1615        // Lookup should find in baseline
1616        let found = handle.get_stake_account_from_pending(&pubkey);
1617        assert!(found.is_some());
1618        assert_eq!(found.unwrap().kelvins, 3000);
1619    }
1620
1621    #[test]
1622    fn test_layered_lookup_priority_pending_over_frozen() {
1623        let pubkey = Pubkey::new_unique();
1624        let validator = Pubkey::new_unique();
1625        let handle = StakesHandle::default();
1626
1627        // Insert into pending with value 1000
1628        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
1629        // Freeze it
1630        handle.freeze_stakes();
1631
1632        // Insert into pending again with value 2000 (overwrites for next epoch)
1633        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
1634
1635        // Lookup from pending should find 2000 (pending wins)
1636        let found = handle.get_stake_account_from_pending(&pubkey);
1637        assert!(found.is_some());
1638        assert_eq!(found.unwrap().kelvins, 2000);
1639
1640        // Lookup from last frozen should find 1000 (skips pending)
1641        let found_frozen = handle.get_stake_account_from_last_frozen(&pubkey);
1642        assert!(found_frozen.is_some());
1643        assert_eq!(found_frozen.unwrap().kelvins, 1000);
1644    }
1645
1646    #[test]
1647    fn test_layered_lookup_priority_frozen_over_baseline() {
1648        let pubkey = Pubkey::new_unique();
1649        let validator = Pubkey::new_unique();
1650
1651        // Create baseline with value 1000
1652        let mut baseline_data = StakeCacheData::default();
1653        baseline_data
1654            .stake_accounts
1655            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
1656        let baseline = StakeCache::with_data(baseline_data);
1657        let handle = StakesHandle::new_shared(
1658            baseline,
1659            StakeCache::default(),
1660            StakeHistory::default(),
1661            Arc::new(|_| false),
1662        );
1663
1664        // Insert into pending with value 2000 and freeze
1665        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
1666        handle.freeze_stakes();
1667
1668        // Lookup should find 2000 (frozen wins over baseline)
1669        let found = handle.get_stake_account_from_pending(&pubkey);
1670        assert!(found.is_some());
1671        assert_eq!(found.unwrap().kelvins, 2000);
1672    }
1673
1674    #[test]
1675    fn test_layered_lookup_multiple_frozen_epochs() {
1676        let pubkey = Pubkey::new_unique();
1677        let validator = Pubkey::new_unique();
1678        let handle = StakesHandle::default();
1679
1680        // Epoch 1: Insert and freeze with value 1000
1681        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
1682        handle.freeze_stakes();
1683
1684        // Epoch 2: Insert and freeze with value 2000
1685        handle.insert_stake_account(pubkey, create_test_stake_account(2000, validator));
1686        handle.freeze_stakes();
1687
1688        // Epoch 3: Insert and freeze with value 3000
1689        handle.insert_stake_account(pubkey, create_test_stake_account(3000, validator));
1690        handle.freeze_stakes();
1691
1692        // Lookup from last frozen should find 3000 (newest frozen)
1693        let found = handle.get_stake_account_from_last_frozen(&pubkey);
1694        assert!(found.is_some());
1695        assert_eq!(found.unwrap().kelvins, 3000);
1696
1697        // Verify frozen history has 3 entries
1698        assert_eq!(handle.frozen_len(), 3);
1699    }
1700
1701    #[test]
1702    fn test_layered_lookup_validator_account() {
1703        let pubkey = Pubkey::new_unique();
1704
1705        // Create baseline with validator
1706        let mut baseline_data = StakeCacheData::default();
1707        baseline_data
1708            .validator_accounts
1709            .insert(pubkey, Some(create_test_validator_account(1000, 500)));
1710        let baseline = StakeCache::with_data(baseline_data);
1711        let handle = StakesHandle::new_shared(
1712            baseline,
1713            StakeCache::default(),
1714            StakeHistory::default(),
1715            Arc::new(|_| false),
1716        );
1717
1718        // Lookup should find in baseline
1719        let found = handle.get_validator_account_from_pending(&pubkey);
1720        assert!(found.is_some());
1721        assert_eq!(found.unwrap().kelvins, 1000);
1722
1723        // Add update in pending
1724        handle.insert_validator_account(pubkey, create_test_validator_account(2000, 600));
1725
1726        // Lookup should now find pending value
1727        let found = handle.get_validator_account_from_pending(&pubkey);
1728        assert!(found.is_some());
1729        assert_eq!(found.unwrap().kelvins, 2000);
1730    }
1731
1732    // ========================================================================
1733    // Tombstone Handling Tests
1734    // ========================================================================
1735
1736    #[test]
1737    fn test_tombstone_in_pending_hides_frozen() {
1738        let pubkey = Pubkey::new_unique();
1739        let validator = Pubkey::new_unique();
1740        let handle = StakesHandle::default();
1741
1742        // Insert and freeze
1743        handle.insert_stake_account(pubkey, create_test_stake_account(1000, validator));
1744        handle.freeze_stakes();
1745
1746        // Add tombstone in pending (marks as deleted for next epoch)
1747        handle.raw_pending().tombstone_stake_account(pubkey);
1748
1749        // Lookup from pending should return None (tombstone = deleted)
1750        let found = handle.get_stake_account_from_pending(&pubkey);
1751        assert!(
1752            found.is_none(),
1753            "Tombstone in pending should hide frozen value"
1754        );
1755
1756        // Lookup from last frozen should still find the value (skips pending)
1757        let found_frozen = handle.get_stake_account_from_last_frozen(&pubkey);
1758        assert!(found_frozen.is_some());
1759        assert_eq!(found_frozen.unwrap().kelvins, 1000);
1760    }
1761
1762    #[test]
1763    fn test_tombstone_in_frozen_hides_baseline() {
1764        let pubkey = Pubkey::new_unique();
1765        let validator = Pubkey::new_unique();
1766
1767        // Create baseline with account
1768        let mut baseline_data = StakeCacheData::default();
1769        baseline_data
1770            .stake_accounts
1771            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
1772        let baseline = StakeCache::with_data(baseline_data);
1773        let handle = StakesHandle::new_shared(
1774            baseline,
1775            StakeCache::default(),
1776            StakeHistory::default(),
1777            Arc::new(|_| false),
1778        );
1779
1780        // Add tombstone in pending and freeze
1781        handle.raw_pending().tombstone_stake_account(pubkey);
1782        handle.freeze_stakes();
1783
1784        // Lookup from last frozen should return None (tombstone hides baseline)
1785        let found = handle.get_stake_account_from_last_frozen(&pubkey);
1786        assert!(
1787            found.is_none(),
1788            "Tombstone in frozen should hide baseline value"
1789        );
1790
1791        // First frozen lookup should also see tombstone
1792        let found_first = handle.get_stake_account_from_first_frozen(&pubkey);
1793        assert!(found_first.is_none());
1794    }
1795
1796    #[test]
1797    fn test_tombstone_validator_account() {
1798        let pubkey = Pubkey::new_unique();
1799
1800        // Create baseline with validator
1801        let mut baseline_data = StakeCacheData::default();
1802        baseline_data
1803            .validator_accounts
1804            .insert(pubkey, Some(create_test_validator_account(1000, 500)));
1805        let baseline = StakeCache::with_data(baseline_data);
1806        let handle = StakesHandle::new_shared(
1807            baseline,
1808            StakeCache::default(),
1809            StakeHistory::default(),
1810            Arc::new(|_| false),
1811        );
1812
1813        // Lookup should find in baseline initially
1814        assert!(handle.get_validator_account_from_pending(&pubkey).is_some());
1815
1816        // Add tombstone in pending
1817        handle.raw_pending().tombstone_validator_account(pubkey);
1818
1819        // Lookup from pending should now return None
1820        let found = handle.get_validator_account_from_pending(&pubkey);
1821        assert!(found.is_none(), "Tombstone should hide baseline validator");
1822    }
1823
1824    #[test]
1825    fn test_get_all_validators_excludes_tombstones() {
1826        let pubkey1 = Pubkey::new_unique();
1827        let pubkey2 = Pubkey::new_unique();
1828
1829        // Create baseline with two validators
1830        let mut baseline_data = StakeCacheData::default();
1831        baseline_data
1832            .validator_accounts
1833            .insert(pubkey1, Some(create_test_validator_account(1000, 100)));
1834        baseline_data
1835            .validator_accounts
1836            .insert(pubkey2, Some(create_test_validator_account(2000, 200)));
1837        let baseline = StakeCache::with_data(baseline_data);
1838        let handle = StakesHandle::new_shared(
1839            baseline,
1840            StakeCache::default(),
1841            StakeHistory::default(),
1842            Arc::new(|_| false),
1843        );
1844
1845        // Initially should have 2 validators
1846        let all = handle.get_all_validator_accounts_from_pending();
1847        assert_eq!(all.len(), 2);
1848
1849        // Add tombstone for pubkey1 in pending
1850        handle.raw_pending().tombstone_validator_account(pubkey1);
1851
1852        // Now should only have 1 validator (pubkey2)
1853        let all = handle.get_all_validator_accounts_from_pending();
1854        assert_eq!(all.len(), 1);
1855        assert_eq!(all[0].0, pubkey2);
1856    }
1857
1858    #[test]
1859    fn test_tombstone_then_readd() {
1860        let pubkey = Pubkey::new_unique();
1861        let validator = Pubkey::new_unique();
1862
1863        // Create baseline with account
1864        let mut baseline_data = StakeCacheData::default();
1865        baseline_data
1866            .stake_accounts
1867            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
1868        let baseline = StakeCache::with_data(baseline_data);
1869        let handle = StakesHandle::new_shared(
1870            baseline,
1871            StakeCache::default(),
1872            StakeHistory::default(),
1873            Arc::new(|_| false),
1874        );
1875
1876        // Delete in epoch 1
1877        handle.raw_pending().tombstone_stake_account(pubkey);
1878        handle.freeze_stakes();
1879
1880        // Should be deleted
1881        let found = handle.get_stake_account_from_last_frozen(&pubkey);
1882        assert!(found.is_none());
1883
1884        // Re-add in epoch 2 with new value
1885        handle.insert_stake_account(pubkey, create_test_stake_account(5000, validator));
1886        handle.freeze_stakes();
1887
1888        // Should be visible again with new value
1889        let found = handle.get_stake_account_from_last_frozen(&pubkey);
1890        assert!(found.is_some());
1891        assert_eq!(found.unwrap().kelvins, 5000);
1892    }
1893
1894    // ========================================================================
1895    // Empty Epoch Handling Tests
1896    // ========================================================================
1897
1898    #[test]
1899    fn test_empty_pending_freeze() {
1900        let handle = StakesHandle::default();
1901
1902        // Freeze with empty pending
1903        handle.freeze_stakes();
1904
1905        // Frozen should have 1 entry (empty delta)
1906        assert_eq!(handle.frozen_len(), 1);
1907
1908        // Lookup should still work (returns None for nonexistent)
1909        let pubkey = Pubkey::new_unique();
1910        assert!(handle.get_stake_account_from_pending(&pubkey).is_none());
1911    }
1912
1913    #[test]
1914    fn test_empty_frozen_epochs() {
1915        let pubkey = Pubkey::new_unique();
1916        let validator = Pubkey::new_unique();
1917
1918        // Create baseline with account
1919        let mut baseline_data = StakeCacheData::default();
1920        baseline_data
1921            .stake_accounts
1922            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
1923        let baseline = StakeCache::with_data(baseline_data);
1924        let handle = StakesHandle::new_shared(
1925            baseline,
1926            StakeCache::default(),
1927            StakeHistory::default(),
1928            Arc::new(|_| false),
1929        );
1930
1931        // Freeze several empty epochs
1932        handle.freeze_stakes();
1933        handle.freeze_stakes();
1934        handle.freeze_stakes();
1935
1936        // Lookup should still find baseline value through empty frozen epochs
1937        let found = handle.get_stake_account_from_pending(&pubkey);
1938        assert!(found.is_some());
1939        assert_eq!(found.unwrap().kelvins, 1000);
1940    }
1941
1942    #[test]
1943    fn test_no_frozen_epochs_falls_through_to_baseline() {
1944        let pubkey = Pubkey::new_unique();
1945        let validator = Pubkey::new_unique();
1946
1947        // Create baseline with account, no frozen history
1948        let mut baseline_data = StakeCacheData::default();
1949        baseline_data
1950            .stake_accounts
1951            .insert(pubkey, Some(create_test_stake_account(1000, validator)));
1952        let baseline = StakeCache::with_data(baseline_data);
1953        let handle = StakesHandle::new_shared(
1954            baseline,
1955            StakeCache::default(),
1956            StakeHistory::default(),
1957            Arc::new(|_| false),
1958        );
1959
1960        // Lookup from last frozen should fall through to baseline
1961        let found = handle.get_stake_account_from_last_frozen(&pubkey);
1962        assert!(found.is_some());
1963        assert_eq!(found.unwrap().kelvins, 1000);
1964    }
1965
1966    #[test]
1967    fn test_get_all_stake_accounts_from_frozen_epoch() {
1968        // Test that from_frozen_epoch only includes deltas up to the target epoch
1969        let validator = Pubkey::new_unique();
1970
1971        // Baseline: one account
1972        let baseline_stake = Pubkey::new_unique();
1973        let mut baseline_data = StakeCacheData::default();
1974        baseline_data.stake_accounts.insert(
1975            baseline_stake,
1976            Some(create_test_stake_account(1000, validator)),
1977        );
1978        let baseline = StakeCache::with_data(baseline_data);
1979        let handle = StakesHandle::new_shared(
1980            baseline,
1981            StakeCache::default(),
1982            StakeHistory::default(),
1983            Arc::new(|_| false),
1984        );
1985
1986        // Epoch 5: Add stake_epoch5
1987        let stake_epoch5 = Pubkey::new_unique();
1988        handle.set_pending_epoch(5);
1989        handle.insert_stake_account(stake_epoch5, create_test_stake_account(2000, validator));
1990        handle.freeze_stakes();
1991
1992        // Epoch 6: Add stake_epoch6
1993        let stake_epoch6 = Pubkey::new_unique();
1994        handle.insert_stake_account(stake_epoch6, create_test_stake_account(3000, validator));
1995        handle.freeze_stakes();
1996
1997        // Epoch 7: Add stake_epoch7
1998        let stake_epoch7 = Pubkey::new_unique();
1999        handle.insert_stake_account(stake_epoch7, create_test_stake_account(4000, validator));
2000        handle.freeze_stakes();
2001
2002        // Verify: from_frozen_epoch(5) should include baseline + epoch 5 only
2003        let accounts_epoch5 = handle.get_all_stake_accounts_from_frozen_epoch(5);
2004        assert_eq!(accounts_epoch5.len(), 2); // baseline + epoch5
2005        assert!(accounts_epoch5.iter().any(|(k, _)| *k == baseline_stake));
2006        assert!(accounts_epoch5.iter().any(|(k, _)| *k == stake_epoch5));
2007        assert!(!accounts_epoch5.iter().any(|(k, _)| *k == stake_epoch6));
2008
2009        // Verify: from_frozen_epoch(6) should include baseline + epoch 5 + epoch 6
2010        let accounts_epoch6 = handle.get_all_stake_accounts_from_frozen_epoch(6);
2011        assert_eq!(accounts_epoch6.len(), 3);
2012        assert!(accounts_epoch6.iter().any(|(k, _)| *k == stake_epoch6));
2013        assert!(!accounts_epoch6.iter().any(|(k, _)| *k == stake_epoch7));
2014
2015        // Verify: from_frozen_epoch(7) should include all 4
2016        let accounts_epoch7 = handle.get_all_stake_accounts_from_frozen_epoch(7);
2017        assert_eq!(accounts_epoch7.len(), 4);
2018        assert!(accounts_epoch7.iter().any(|(k, _)| *k == stake_epoch7));
2019    }
2020
2021    #[test]
2022    fn test_get_all_validators_with_no_validators() {
2023        let handle = StakesHandle::default();
2024
2025        // No validators anywhere
2026        let all = handle.get_all_validator_accounts_from_pending();
2027        assert!(all.is_empty());
2028
2029        // Freeze and check again
2030        handle.freeze_stakes();
2031        let all = handle.get_all_validator_accounts_from_last_frozen();
2032        assert!(all.is_empty());
2033    }
2034}