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