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