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