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