Skip to main content

rialo_s_program_runtime/
invoke_context.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3// This file is either (a) original to Subzero Labs, Inc. or (b) derived from the Anza codebase and modified by Subzero Labs, Inc.
4
5use std::{cell::RefCell, collections::HashSet, rc::Rc};
6
7use rialo_hash::Hash;
8use rialo_s_account::{create_account_shared_data_for_test, AccountSharedData, StoredAccount};
9use rialo_s_compute_budget::compute_budget::ComputeBudget;
10use rialo_s_epoch_schedule::EpochSchedule;
11use rialo_s_instruction::{error::InstructionError, AccountMeta};
12use rialo_s_log_collector::{ic_msg, LogCollector};
13use rialo_s_measure::measure::Measure;
14use rialo_s_precompiles::Precompile;
15use rialo_s_pubkey::Pubkey;
16use rialo_s_sdk_ids::{bpf_loader_deprecated, native_loader, sysvar};
17use rialo_s_stable_layout::stable_instruction::StableInstruction;
18use rialo_s_timings::{ExecuteDetailsTimings, ExecuteTimings};
19use rialo_s_transaction_context::{
20    IndexOfAccount, InstructionAccount, TransactionAccount, TransactionContext,
21};
22use rialo_s_type_overrides::sync::{atomic::Ordering, Arc};
23pub use rialo_stake_cache_interface::{StakeCacheData, StakesHandle, ValidatorAccount};
24
25use crate::{
26    active_features::ActiveFeatures,
27    loaded_programs::{ProgramCacheEntryType, ProgramCacheForTx},
28    stable_log,
29    sysvar_cache::SysvarCache,
30};
31
32pub type BuiltinFunctionWithContext =
33    fn(&mut InvokeContext<'_, '_>) -> Result<(), InstructionError>;
34
35/// Adapter so we can unify the interfaces of built-in programs and syscalls
36#[macro_export]
37macro_rules! declare_process_instruction {
38    ($process_instruction:ident, $cu_to_consume:expr, |$invoke_context:ident| $inner:tt) => {
39        pub struct $process_instruction {}
40
41        impl $process_instruction {
42            pub fn vm(
43                invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>,
44            ) -> std::result::Result<(), $crate::__private::InstructionError> {
45                fn process_instruction_inner(
46                    $invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>,
47                ) -> std::result::Result<(), $crate::__private::InstructionError>
48                    $inner
49
50                let consumption_result = if $cu_to_consume > 0 {
51                    invoke_context.consume_checked($cu_to_consume)
52                } else {
53                    Ok(())
54                };
55
56                consumption_result.and_then(|_| process_instruction_inner(invoke_context))
57            }
58        }
59    };
60}
61
62/// Account insert error.
63pub enum AccountInsertError {
64    /// Too many accounts have been loaded.
65    TooManyAccounts,
66
67    /// Account has already been loaded.
68    AccountAlreadyLoaded,
69}
70
71/// Runtime account loader.
72///
73/// Used by the RISC-V VM to load accounts into VM during runtime of the program.
74#[allow(clippy::result_unit_err)]
75pub trait RuntimeAccountLoader {
76    /// Attempt to load the account info of `pubkey`.
77    ///
78    /// Returns `Ok(Some(AccountSharedData))` on success.
79    /// Returns `Ok(None)` if the account was not found.
80    /// Returns `Err(())` if there was an error with account loading that should abort the program.
81    fn load_account(&self, pubkey: &Pubkey) -> Result<Option<StoredAccount>, ()>;
82}
83
84pub struct EnvironmentConfig<'a> {
85    pub blockhash: Hash,
86    pub blockhash_kelvins_per_signature: u64,
87    get_epoch_vote_account_stake_callback: &'a dyn Fn(&'a Pubkey) -> u64,
88    /// Per-block `ActiveFeatures` snapshot from
89    /// `rialo-feature-management-enforcement-interface`. Sole per-block feature view.
90    pub active_features: Arc<ActiveFeatures>,
91    sysvar_cache: &'a SysvarCache,
92    pub random_seed: u64,
93    /// Handle to the stake cache used during instruction execution.
94    /// Builtin programs interact with the cache through this handle.
95    stakes_handle: StakesHandle,
96}
97impl<'a> EnvironmentConfig<'a> {
98    pub fn new(
99        blockhash: Hash,
100        blockhash_kelvins_per_signature: u64,
101        get_epoch_vote_account_stake_callback: &'a dyn Fn(&'a Pubkey) -> u64,
102        active_features: Arc<ActiveFeatures>,
103        sysvar_cache: &'a SysvarCache,
104        random_seed: u64,
105        stakes_handle: StakesHandle,
106    ) -> Self {
107        Self {
108            blockhash,
109            blockhash_kelvins_per_signature,
110            get_epoch_vote_account_stake_callback,
111            active_features,
112            sysvar_cache,
113            random_seed,
114            stakes_handle,
115        }
116    }
117}
118
119pub struct SyscallContext {
120    pub accounts_metadata: Vec<SerializedAccountMetadata>,
121}
122
123#[derive(Debug, Clone)]
124pub struct SerializedAccountMetadata {
125    pub original_data_len: usize,
126    pub vm_data_addr: u64,
127    pub vm_key_addr: u64,
128    pub vm_kelvins_addr: u64,
129    pub vm_owner_addr: u64,
130}
131
132/// Main pipeline from runtime to program execution.
133pub struct InvokeContext<'a, 'b> {
134    /// Information about the currently executing transaction.
135    pub transaction_context: &'a mut TransactionContext,
136    /// The local program cache for the transaction batch.
137    pub program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
138    /// Runtime configurations used to provision the invocation environment.
139    pub environment_config: EnvironmentConfig<'a>,
140    /// The compute budget for the current invocation.
141    compute_budget: ComputeBudget,
142    /// Instruction compute meter, for tracking compute units consumed against
143    /// the designated compute budget during program execution.
144    compute_meter: RefCell<u64>,
145    log_collector: Option<Rc<RefCell<LogCollector>>>,
146    /// Latest measurement not yet accumulated in [ExecuteDetailsTimings::execute_us]
147    pub execute_time: Option<Measure>,
148    pub timings: ExecuteDetailsTimings,
149    pub syscall_context: Vec<Option<SyscallContext>>,
150    pub account_loader: Option<Box<dyn RuntimeAccountLoader + 'a>>,
151    /// Map of accounts already loaded in the VM.
152    ///
153    /// This is shared across CPI calls of the program and is used to prevent programs from reloading accounts
154    /// and/or loading accounts other programs have already loaded.
155    loaded_accounts: HashSet<Pubkey>,
156    /// How many writable accounts can be loaded.
157    num_account_loads: usize,
158}
159
160impl<'a, 'b> InvokeContext<'a, 'b> {
161    #[allow(clippy::too_many_arguments)]
162    pub fn new(
163        transaction_context: &'a mut TransactionContext,
164        program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
165        environment_config: EnvironmentConfig<'a>,
166        log_collector: Option<Rc<RefCell<LogCollector>>>,
167        compute_budget: ComputeBudget,
168    ) -> Self {
169        let loaded_accounts = transaction_context.account_keys().copied().collect();
170
171        Self {
172            transaction_context,
173            program_cache_for_tx_batch,
174            environment_config,
175            log_collector,
176            compute_budget,
177            compute_meter: RefCell::new(compute_budget.compute_unit_limit),
178            execute_time: None,
179            timings: ExecuteDetailsTimings::default(),
180            syscall_context: Vec::new(),
181            account_loader: None,
182            loaded_accounts,
183            num_account_loads: 0usize,
184        }
185    }
186
187    pub fn new_with_account_loader(
188        transaction_context: &'a mut TransactionContext,
189        program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
190        environment_config: EnvironmentConfig<'a>,
191        log_collector: Option<Rc<RefCell<LogCollector>>>,
192        compute_budget: ComputeBudget,
193        account_loader: Box<dyn RuntimeAccountLoader + 'a>,
194        num_account_locks: usize,
195        num_writable_accounts: usize,
196    ) -> Self {
197        let loaded_accounts = transaction_context.account_keys().copied().collect();
198
199        Self {
200            transaction_context,
201            program_cache_for_tx_batch,
202            environment_config,
203            log_collector,
204            compute_budget,
205            compute_meter: RefCell::new(compute_budget.compute_unit_limit),
206            execute_time: None,
207            timings: ExecuteDetailsTimings::default(),
208            syscall_context: Vec::new(),
209            account_loader: Some(account_loader),
210            loaded_accounts,
211            num_account_loads: num_account_locks.saturating_sub(num_writable_accounts),
212        }
213    }
214
215    /// Attempt to loaded account `pubkey` into `InvokeContext`.
216    ///
217    /// Returns `Ok(())` on success
218    /// Returns `Err(AccountInsertError::TooManyAccounts)` if too many accounts have been loaded.
219    /// Returns `Err(AccountInsertError::AccountAlreadyLoaded)` if account has already been loaded.
220    pub fn add_loaded_account(&mut self, pubkey: Pubkey) -> Result<(), AccountInsertError> {
221        match self.num_account_loads.checked_sub(1) {
222            Some(value) => self.num_account_loads = value,
223            None => return Err(AccountInsertError::TooManyAccounts),
224        }
225
226        if self.loaded_accounts.insert(pubkey) {
227            return Ok(());
228        }
229
230        Err(AccountInsertError::AccountAlreadyLoaded)
231    }
232
233    /// Push a stack frame onto the invocation stack
234    pub fn push(&mut self) -> Result<(), InstructionError> {
235        let instruction_context = self
236            .transaction_context
237            .get_instruction_context_at_index_in_trace(
238                self.transaction_context.get_instruction_trace_length(),
239            )?;
240        let program_id = instruction_context
241            .get_last_program_key(self.transaction_context)
242            .map_err(|_| InstructionError::UnsupportedProgramId)?;
243        if self
244            .transaction_context
245            .get_instruction_context_stack_height()
246            != 0
247        {
248            let contains = (0..self
249                .transaction_context
250                .get_instruction_context_stack_height())
251                .any(|level| {
252                    self.transaction_context
253                        .get_instruction_context_at_nesting_level(level)
254                        .and_then(|instruction_context| {
255                            instruction_context
256                                .try_borrow_last_program_account(self.transaction_context)
257                        })
258                        .map(|program_account| program_account.get_key() == program_id)
259                        .unwrap_or(false)
260                });
261            let is_last = self
262                .transaction_context
263                .get_current_instruction_context()
264                .and_then(|instruction_context| {
265                    instruction_context.try_borrow_last_program_account(self.transaction_context)
266                })
267                .map(|program_account| program_account.get_key() == program_id)
268                .unwrap_or(false);
269            if contains && !is_last {
270                // Reentrancy not allowed unless caller is calling itself
271                return Err(InstructionError::ReentrancyNotAllowed);
272            }
273        }
274
275        self.syscall_context.push(None);
276        self.transaction_context.push()
277    }
278
279    /// Pop a stack frame from the invocation stack
280    fn pop(&mut self) -> Result<(), InstructionError> {
281        self.syscall_context.pop();
282        self.transaction_context.pop()
283    }
284
285    /// Current height of the invocation stack, top level instructions are height
286    /// `rialo_s_instruction::TRANSACTION_LEVEL_STACK_HEIGHT`
287    pub fn get_stack_height(&self) -> usize {
288        self.transaction_context
289            .get_instruction_context_stack_height()
290    }
291
292    /// Entrypoint for a cross-program invocation from a builtin program
293    pub fn native_invoke(
294        &mut self,
295        instruction: StableInstruction,
296        signers: &[Pubkey],
297    ) -> Result<(), InstructionError> {
298        let (instruction_accounts, program_indices) =
299            self.prepare_instruction(&instruction, signers)?;
300        let mut compute_units_consumed = 0;
301        self.process_instruction(
302            &instruction.data,
303            &instruction_accounts,
304            &program_indices,
305            &mut compute_units_consumed,
306            &mut ExecuteTimings::default(),
307        )?;
308        Ok(())
309    }
310
311    /// Helper to prepare for process_instruction()
312    #[allow(clippy::type_complexity)]
313    pub fn prepare_instruction(
314        &mut self,
315        instruction: &StableInstruction,
316        signers: &[Pubkey],
317    ) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
318        self.prepare_instruction_inner(instruction.program_id, &instruction.accounts, signers)
319    }
320
321    /// Helper to prepare for process_instruction()
322    pub fn prepare_cpi_instruction(
323        &mut self,
324        program_id: Pubkey,
325        account_metas: &[AccountMeta],
326        signers: &[Pubkey],
327    ) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
328        self.prepare_instruction_inner(program_id, account_metas, signers)
329    }
330
331    pub fn prepare_instruction_inner(
332        &mut self,
333        callee_program_id: Pubkey,
334        account_metas: &[AccountMeta],
335        signers: &[Pubkey],
336    ) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
337        // Finds the index of each account in the instruction by its pubkey.
338        // Then normalizes / unifies the privileges of duplicate accounts.
339        // Note: This is an O(n^2) algorithm,
340        // but performed on a very small slice and requires no heap allocations.
341        let instruction_context = self.transaction_context.get_current_instruction_context()?;
342        let mut deduplicated_instruction_accounts: Vec<InstructionAccount> = Vec::new();
343        let mut duplicate_indicies = Vec::with_capacity(account_metas.len());
344        for (instruction_account_index, account_meta) in account_metas.iter().enumerate() {
345            let index_in_transaction = self
346                .transaction_context
347                .find_index_of_account(&account_meta.pubkey)
348                .ok_or_else(|| {
349                    ic_msg!(
350                        self,
351                        "Instruction references an unknown account {}",
352                        account_meta.pubkey,
353                    );
354                    InstructionError::MissingAccount
355                })?;
356            if let Some(duplicate_index) =
357                deduplicated_instruction_accounts
358                    .iter()
359                    .position(|instruction_account| {
360                        instruction_account.index_in_transaction == index_in_transaction
361                    })
362            {
363                duplicate_indicies.push(duplicate_index);
364                let instruction_account = deduplicated_instruction_accounts
365                    .get_mut(duplicate_index)
366                    .ok_or(InstructionError::NotEnoughAccountKeys)?;
367                instruction_account.is_signer |= account_meta.is_signer;
368                instruction_account.is_writable |= account_meta.is_writable;
369            } else {
370                let index_in_caller = instruction_context
371                    .find_index_of_instruction_account(
372                        self.transaction_context,
373                        &account_meta.pubkey,
374                    )
375                    .ok_or_else(|| {
376                        ic_msg!(
377                            self,
378                            "Instruction references an unknown account {}",
379                            account_meta.pubkey,
380                        );
381                        InstructionError::MissingAccount
382                    })?;
383                duplicate_indicies.push(deduplicated_instruction_accounts.len());
384                deduplicated_instruction_accounts.push(InstructionAccount {
385                    index_in_transaction,
386                    index_in_caller,
387                    index_in_callee: instruction_account_index as IndexOfAccount,
388                    is_signer: account_meta.is_signer,
389                    is_writable: account_meta.is_writable,
390                });
391            }
392        }
393        for instruction_account in deduplicated_instruction_accounts.iter() {
394            let borrowed_account = instruction_context.try_borrow_instruction_account(
395                self.transaction_context,
396                instruction_account.index_in_caller,
397            )?;
398
399            // Readonly in caller cannot become writable in callee
400            if instruction_account.is_writable && !borrowed_account.is_writable() {
401                ic_msg!(
402                    self,
403                    "{}'s writable privilege escalated",
404                    borrowed_account.get_key(),
405                );
406                return Err(InstructionError::PrivilegeEscalation);
407            }
408
409            // To be signed in the callee,
410            // it must be either signed in the caller or by the program
411            if instruction_account.is_signer
412                && !(borrowed_account.is_signer() || signers.contains(borrowed_account.get_key()))
413            {
414                ic_msg!(
415                    self,
416                    "{}'s signer privilege escalated",
417                    borrowed_account.get_key()
418                );
419                return Err(InstructionError::PrivilegeEscalation);
420            }
421        }
422        let instruction_accounts = duplicate_indicies
423            .into_iter()
424            .map(|duplicate_index| {
425                deduplicated_instruction_accounts
426                    .get(duplicate_index)
427                    .cloned()
428                    .ok_or(InstructionError::NotEnoughAccountKeys)
429            })
430            .collect::<Result<Vec<InstructionAccount>, InstructionError>>()?;
431
432        // Find and validate executables / program accounts. The legacy
433        // `lift_cpi_caller_restriction` feature is unconditionally active in
434        // Rialo (every Solana feature is enabled at slot 0), so only the
435        // "lifted" arm survives.
436        let program_account_index = match (
437            self.transaction_context
438                .find_index_of_program_account(&callee_program_id),
439            &self.account_loader,
440        ) {
441            (Some(index), _) => index,
442            (None, Some(account_loader)) => match account_loader.load_account(&callee_program_id) {
443                Ok(Some(account)) => {
444                    self.transaction_context
445                        .add_account((callee_program_id, account.clone()));
446
447                    // program must not exist in `loaded_accounts` since it was not found in transaction context
448                    assert!(self.loaded_accounts.insert(callee_program_id));
449
450                    // program must exist since it was just added into transaction context
451                    self.transaction_context
452                        .find_index_of_program_account(&callee_program_id)
453                        .expect("program to exist")
454                }
455                Ok(None) => return Err(InstructionError::MissingAccount),
456                Err(()) => return Err(InstructionError::GenericError),
457            },
458            (None, None) => {
459                ic_msg!(self, "Unknown program {}", callee_program_id);
460                return Err(InstructionError::MissingAccount);
461            }
462        };
463
464        Ok((instruction_accounts, vec![program_account_index]))
465    }
466
467    /// Processes an instruction and returns how many compute units were used
468    pub fn process_instruction(
469        &mut self,
470        instruction_data: &[u8],
471        instruction_accounts: &[InstructionAccount],
472        program_indices: &[IndexOfAccount],
473        compute_units_consumed: &mut u64,
474        timings: &mut ExecuteTimings,
475    ) -> Result<(), InstructionError> {
476        *compute_units_consumed = 0;
477        self.transaction_context
478            .get_next_instruction_context()?
479            .configure(program_indices, instruction_accounts, instruction_data);
480        self.push()?;
481        self.process_executable_chain(compute_units_consumed, timings)
482            // MUST pop if and only if `push` succeeded, independent of `result`.
483            // Thus, the `.and()` instead of an `.and_then()`.
484            .and(self.pop())
485    }
486
487    /// Processes a precompile instruction
488    pub fn process_precompile<'ix_data>(
489        &mut self,
490        precompile: &Precompile,
491        instruction_data: &[u8],
492        instruction_accounts: &[InstructionAccount],
493        program_indices: &[IndexOfAccount],
494        message_instruction_datas_iter: impl Iterator<Item = &'ix_data [u8]>,
495    ) -> Result<(), InstructionError> {
496        self.transaction_context
497            .get_next_instruction_context()?
498            .configure(program_indices, instruction_accounts, instruction_data);
499        self.push()?;
500
501        // Precompile verification always runs inside the SVM in Rialo;
502        // there is no legacy "verify outside the SVM" path to gate against.
503        let instruction_datas: Vec<_> = message_instruction_datas_iter.collect();
504        precompile
505            .verify(instruction_data, &instruction_datas)
506            .map_err(InstructionError::from)
507            .and(self.pop())
508    }
509
510    /// Calls the instruction's program entrypoint method
511    fn process_executable_chain(
512        &mut self,
513        compute_units_consumed: &mut u64,
514        timings: &mut ExecuteTimings,
515    ) -> Result<(), InstructionError> {
516        let instruction_context = self.transaction_context.get_current_instruction_context()?;
517        let process_executable_chain_time = Measure::start("process_executable_chain_time");
518
519        let builtin_id = {
520            debug_assert!(instruction_context.get_number_of_program_accounts() <= 1);
521            let borrowed_root_account = instruction_context
522                .try_borrow_program_account(self.transaction_context, 0)
523                .map_err(|_| InstructionError::UnsupportedProgramId)?;
524            let owner_id = borrowed_root_account.get_owner();
525            if native_loader::check_id(owner_id) {
526                *borrowed_root_account.get_key()
527            } else {
528                *owner_id
529            }
530        };
531
532        let entry = self
533            .program_cache_for_tx_batch
534            .find(&builtin_id)
535            .ok_or(InstructionError::UnsupportedProgramId)?;
536        let function = match &entry.program {
537            ProgramCacheEntryType::Builtin(function) => *function,
538            _ => return Err(InstructionError::UnsupportedProgramId),
539        };
540        entry.ix_usage_counter.fetch_add(1, Ordering::Relaxed);
541
542        let program_id = *instruction_context.get_last_program_key(self.transaction_context)?;
543        self.transaction_context
544            .set_return_data(program_id, Vec::new())?;
545        let logger = self.get_log_collector();
546        stable_log::program_invoke(&logger, &program_id, self.get_stack_height());
547        let pre_remaining_units = self.get_remaining();
548        let result = match function(self) {
549            Ok(()) => {
550                stable_log::program_success(&logger, &program_id);
551                Ok(())
552            }
553            Err(err) => {
554                stable_log::program_failure(&logger, &program_id, &err);
555                Err(err)
556            }
557        };
558        let post_remaining_units = self.get_remaining();
559        *compute_units_consumed = pre_remaining_units.saturating_sub(post_remaining_units);
560
561        if builtin_id == program_id && result.is_ok() && *compute_units_consumed == 0 {
562            return Err(InstructionError::BuiltinProgramsMustConsumeComputeUnits);
563        }
564
565        timings
566            .execute_accessories
567            .process_instructions
568            .process_executable_chain_us += process_executable_chain_time.end_as_us();
569        result
570    }
571
572    /// Get this invocation's LogCollector
573    pub fn get_log_collector(&self) -> Option<Rc<RefCell<LogCollector>>> {
574        self.log_collector.clone()
575    }
576
577    /// Returns remaining compute units.
578    pub fn get_remaining(&self) -> u64 {
579        *self.compute_meter.borrow()
580    }
581
582    /// Consume compute units
583    pub fn consume_checked(&self, amount: u64) -> Result<(), InstructionError> {
584        let mut compute_meter = self.compute_meter.borrow_mut();
585        let exceeded = *compute_meter < amount;
586        *compute_meter = compute_meter.saturating_sub(amount);
587        if exceeded {
588            return Err(InstructionError::ComputationalBudgetExceeded);
589        }
590        Ok(())
591    }
592
593    /// Set compute units
594    ///
595    /// Only use for tests and benchmarks
596    pub fn mock_set_remaining(&self, remaining: u64) {
597        *self.compute_meter.borrow_mut() = remaining;
598    }
599
600    /// Get this invocation's compute budget
601    pub fn get_compute_budget(&self) -> &ComputeBudget {
602        &self.compute_budget
603    }
604
605    /// Get the per-block `ActiveFeatures` snapshot for the bank executing
606    /// this instruction. The snapshot is frozen for the block — see
607    /// NORTHSTAR §"Per-block view, immutable for the block."
608    pub fn get_active_features(&self) -> &Arc<ActiveFeatures> {
609        &self.environment_config.active_features
610    }
611
612    /// Replace the active-features snapshot. Tests and benchmarks only.
613    ///
614    /// Gated behind `cfg(any(test, feature = "dev-context-only-utils"))`
615    /// so a downstream crate can't accidentally swap a bank's frozen
616    /// per-block view mid-execution and produce inconsistent gating
617    /// decisions inside a single transaction.
618    #[cfg(any(test, feature = "dev-context-only-utils"))]
619    pub fn mock_set_active_features(&mut self, active_features: Arc<ActiveFeatures>) {
620        self.environment_config.active_features = active_features;
621    }
622
623    /// Get cached sysvars
624    pub fn get_sysvar_cache(&self) -> &SysvarCache {
625        self.environment_config.sysvar_cache
626    }
627
628    /// Get cached stake for the epoch vote account.
629    pub fn get_epoch_vote_account_stake(&self, pubkey: &'a Pubkey) -> u64 {
630        (self
631            .environment_config
632            .get_epoch_vote_account_stake_callback)(pubkey)
633    }
634
635    /// Get the CURRENT epoch (the epoch that is actively running).
636    ///
637    /// This is the epoch to use when checking stake activation/deactivation status,
638    /// unbonding periods, lockup periods, etc.
639    ///
640    /// # Implementation Note
641    /// The stake cache stores the NEXT epoch (the epoch being prepared for when
642    /// stake changes become effective). We subtract 1 to get the current epoch.
643    /// `saturating_sub` handles the edge case of epoch 0.
644    pub fn get_current_epoch(&self) -> u64 {
645        self.environment_config
646            .stakes_handle
647            .pending_epoch()
648            .saturating_sub(1)
649    }
650
651    /// Get the NEXT epoch (the epoch being prepared in the stake cache).
652    ///
653    /// This is the epoch that accumulated stake changes will become effective in
654    /// AFTER the next FreezeStakes call. Most instruction logic should use
655    /// `get_current_epoch()` instead; this method is primarily for internal use
656    /// and the FreezeStakes instruction.
657    pub fn get_next_epoch(&self) -> u64 {
658        self.environment_config.stakes_handle.pending_epoch()
659    }
660
661    /// Get the timestamp of the last FreezeStakes call (last epoch boundary).
662    ///
663    /// This is the timestamp from `history.back()` which represents when the most
664    /// recent epoch boundary occurred. Use this for **state transition checks**:
665    /// - A stake is "activating" while `activation_requested >= last_freeze_timestamp`
666    /// - A stake becomes "activated" when `activation_requested < last_freeze_timestamp`
667    /// - A stake is "deactivating" while `deactivation_requested >= last_freeze_timestamp`
668    /// - A stake becomes "deactivated" when `deactivation_requested < last_freeze_timestamp`
669    ///
670    /// Returns 0 if no history exists (should not happen in normal operation since
671    /// `StakingState::new_with_initial_history()` guarantees at least one entry).
672    ///
673    /// **Note:** For duration enforcement (lockup, unbonding), use the current timestamp
674    /// from Clock sysvar instead, to provide real-time guarantees.
675    pub fn get_last_freeze_timestamp(&self) -> u64 {
676        self.environment_config
677            .stakes_handle
678            .last_frozen_timestamp()
679            .unwrap_or(0)
680    }
681
682    /// Set the last freeze timestamp for testing.
683    ///
684    /// This is the primary mock function for timestamp-based tests. It sets up the
685    /// stake cache so that `get_last_freeze_timestamp()` returns the specified timestamp.
686    ///
687    /// The function:
688    /// 1. Sets the pending epoch to 1 (to avoid underflow when computing current epoch)
689    /// 2. Pushes a `StakeCacheData` entry to history with epoch=0 and the specified timestamp
690    ///
691    /// Only use for tests and benchmarks.
692    pub fn mock_set_last_freeze_timestamp(&mut self, timestamp: u64) {
693        // Set pending epoch to 1 to avoid underflow when computing current epoch (pending - 1)
694        self.environment_config.stakes_handle.set_pending_epoch(1);
695
696        // Push a history entry with the specified timestamp
697        let history_entry = StakeCacheData {
698            epoch: 0,
699            timestamp,
700            ..Default::default()
701        };
702        self.environment_config
703            .stakes_handle
704            .push_frozen(history_entry);
705    }
706
707    /// Insert a stake account into the pending cache for testing.
708    ///
709    /// This allows unit tests to set up stake accounts that reference validators,
710    /// which is needed to test the validator withdrawal blocking logic.
711    ///
712    /// Only use for tests and benchmarks.
713    pub fn mock_insert_stake_account(
714        &mut self,
715        pubkey: rialo_s_pubkey::Pubkey,
716        account: rialo_stake_cache_interface::StakeAccount,
717    ) {
718        self.environment_config
719            .stakes_handle
720            .insert_stake_account(pubkey, account);
721    }
722
723    /// Insert a validator account into the pending cache for testing.
724    ///
725    /// Combined with [`InvokeContext::mock_freeze_stakes`], this lets tests
726    /// seed the frozen validator set that
727    /// [`InvokeContext::get_all_validator_accounts_from_last_frozen`] reads —
728    /// for example when exercising builtins (like TokenomicsGovernance's
729    /// `RequestConsensusEpochChange`) that consult the frozen cache for
730    /// committee-membership checks.
731    ///
732    /// Only use for tests and benchmarks.
733    pub fn mock_insert_validator_account(
734        &mut self,
735        pubkey: rialo_s_pubkey::Pubkey,
736        account: ValidatorAccount,
737    ) {
738        self.environment_config
739            .stakes_handle
740            .insert_validator_account(pubkey, account);
741    }
742
743    /// Immediately swap the pending stake cache to frozen for testing.
744    ///
745    /// Production code uses [`InvokeContext::signal_freeze_stakes`] which
746    /// defers the swap to `finalize_impl()`; tests that build state
747    /// inside a `mock_process_instruction` closure need the frozen view
748    /// to be observable from inside the same instruction execution, so
749    /// they call this immediate variant instead.
750    ///
751    /// Only use for tests and benchmarks.
752    pub fn mock_freeze_stakes(&self) {
753        self.environment_config.stakes_handle.freeze_stakes();
754    }
755
756    /// Signal that FreezeStakes was requested without performing the swap.
757    ///
758    /// The actual pending → frozen swap is deferred to `finalize_impl()` to ensure
759    /// pending modifications are persisted first. This only sets the `epoch_stakes_frozen`
760    /// flag, which is read by `apply_pending_validator_changes_if_needed()` and consumed
761    /// by `finalize_impl()`.
762    pub fn signal_freeze_stakes(&self) {
763        self.environment_config
764            .stakes_handle
765            .set_epoch_stakes_frozen();
766    }
767
768    /// Thin wrapper around [`StakesHandle::set_force_next_auto_freeze`].
769    /// See that field's documentation for the end-to-end flow.
770    pub fn signal_force_next_auto_freeze(&self) {
771        self.environment_config
772            .stakes_handle
773            .set_force_next_auto_freeze();
774    }
775
776    /// Thin wrapper around [`StakesHandle::take_force_next_auto_freeze`].
777    /// See that field's documentation for the end-to-end flow.
778    pub fn take_force_next_auto_freeze(&self) -> bool {
779        self.environment_config
780            .stakes_handle
781            .take_force_next_auto_freeze()
782    }
783
784    /// Get all validator accounts from the last frozen epoch (current epoch state).
785    ///
786    /// Returns a vector of `(pubkey, ValidatorAccount)` pairs for all validators,
787    /// sorted by pubkey. This performs a layered lookup: frozen (newest to oldest) → baseline,
788    /// skipping pending (next epoch changes).
789    ///
790    /// This method allows builtin programs (like TokenomicsGovernance) to access
791    /// the frozen validator data directly without needing CPI return data.
792    pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
793        self.environment_config
794            .stakes_handle
795            .get_all_validator_accounts_from_last_frozen()
796    }
797
798    /// Get the length of the frozen stake history.
799    ///
800    /// Used by DistributeRewards to check if there are enough frozen snapshots to merge.
801    /// We require at least 2 entries to allow popping (to preserve the current epoch snapshot).
802    pub fn frozen_stake_history_len(&self) -> usize {
803        self.environment_config.stakes_handle.frozen_len()
804    }
805
806    /// Request epoch rewards initialization.
807    ///
808    /// Called by DistributeRewards instruction to signal the Bank to create an EpochRewards
809    /// account for tracking partitioned reward distribution. The Bank will detect this signal
810    /// after transaction execution and create the account outside of instruction processing.
811    ///
812    /// # Arguments
813    /// * `epoch` - The epoch number for which rewards are being distributed
814    /// * `total_rewards` - Per-epoch inflation budget computed by `DistributeRewards`
815    /// * `total_eligible_stake` - Total eligible stake at the frozen snapshot of `epoch`
816    /// * `validator_scores` - Per-validator scores in basis points, ordered by
817    ///   validator pubkey ascending across the frozen epoch's validator set
818    pub fn request_epoch_rewards_init(
819        &self,
820        epoch: u64,
821        total_rewards: u64,
822        total_eligible_stake: u64,
823        validator_scores: Vec<u32>,
824    ) {
825        self.environment_config
826            .stakes_handle
827            .request_epoch_rewards_init(
828                epoch,
829                total_rewards,
830                total_eligible_stake,
831                validator_scores,
832            );
833    }
834
835    /// Get all validator accounts from baseline + frozen deltas up to and
836    /// including the specified epoch, sorted by pubkey ascending.
837    pub fn get_all_validator_accounts_from_frozen_epoch(
838        &self,
839        epoch: u64,
840    ) -> Vec<(Pubkey, ValidatorAccount)> {
841        self.environment_config
842            .stakes_handle
843            .get_all_validator_accounts_from_frozen_epoch(epoch)
844    }
845
846    /// Get the epoch of the oldest frozen snapshot (front of the queue).
847    ///
848    /// This is the epoch that will be popped by `pop_front_and_merge_to_baseline()`.
849    /// Used by DistributeRewards to know which epoch rewards are being distributed.
850    ///
851    /// Returns `None` if no frozen snapshots exist.
852    pub fn front_frozen_epoch(&self) -> Option<u64> {
853        self.environment_config.stakes_handle.front_frozen_epoch()
854    }
855
856    /// Check if any stake account references the given validator whose unbonding
857    /// period is NOT yet complete.
858    ///
859    /// This performs an O(n) search over all stake accounts using a layered lookup
860    /// (pending → frozen → baseline) to determine if the validator is referenced
861    /// by an active or still-unbonding stake.
862    ///
863    /// Used by ValidatorRegistry's Withdraw handler to prevent withdrawal below
864    /// rent-exempt minimum while stake accounts still reference the validator.
865    ///
866    /// A stake is considered to "reference" the validator if:
867    /// - It is delegated to this validator, AND
868    /// - Either active (no deactivation) OR still unbonding (unbonding not complete)
869    ///
870    /// Stake accounts whose unbonding is complete are NOT counted, since they can
871    /// be fully withdrawn or reactivated to another validator.
872    ///
873    /// # Arguments
874    /// * `validator` - The validator pubkey to check
875    /// * `validator_info` - The validator's info (used to compute unbonding end via `end_of_unbonding`)
876    /// * `last_freeze_timestamp` - Timestamp from last FreezeStakes (epoch boundary)
877    /// * `current_timestamp` - Current block timestamp from Clock sysvar
878    pub fn is_validator_referenced(
879        &self,
880        validator: &Pubkey,
881        validator_info: &rialo_stake_cache_interface::ValidatorInfo,
882        last_freeze_timestamp: u64,
883        current_timestamp: u64,
884    ) -> bool {
885        self.environment_config
886            .stakes_handle
887            .is_validator_referenced(
888                validator,
889                validator_info,
890                last_freeze_timestamp,
891                current_timestamp,
892            )
893    }
894
895    /// Check if any stake account delegated to the given validator is still within
896    /// its lockup period.
897    ///
898    /// Delegates to `StakesHandle::has_locked_stakers()` which performs an O(n) scan
899    /// over all stake accounts. A staker is "locked" if their
900    /// `activation_requested + lockup_period > current_timestamp`.
901    ///
902    /// # Arguments
903    /// * `validator` - The validator pubkey to check
904    /// * `lockup_period` - The validator's lockup period in milliseconds
905    /// * `current_timestamp` - Current block timestamp from Clock sysvar (in milliseconds)
906    ///
907    /// # Returns
908    /// `true` if at least one staker is still within their lockup period.
909    pub fn has_locked_stakers(
910        &self,
911        validator: &Pubkey,
912        lockup_period: u64,
913        current_timestamp: u64,
914    ) -> bool {
915        self.environment_config.stakes_handle.has_locked_stakers(
916            validator,
917            lockup_period,
918            current_timestamp,
919        )
920    }
921
922    /// Check if a validator is referenced by any stake accounts (excluding the self-bond).
923    ///
924    /// This variant excludes the self-bond PDA from the check to prevent circular logic
925    /// where the self-bond cannot be deactivated because its existence always makes
926    /// is_validator_referenced() return true.
927    pub fn is_validator_referenced_excluding_self_bond(
928        &self,
929        validator_pubkey: &Pubkey,
930        validator_info: &rialo_stake_cache_interface::ValidatorInfo,
931        last_freeze_timestamp: u64,
932        current_timestamp: u64,
933    ) -> bool {
934        self.environment_config
935            .stakes_handle
936            .is_validator_referenced_excluding_self_bond(
937                validator_pubkey,
938                validator_info,
939                last_freeze_timestamp,
940                current_timestamp,
941            )
942    }
943
944    /// Look up a stake account by pubkey from the pending layer (next epoch state).
945    ///
946    /// Searches: pending → frozen (newest to oldest) → baseline.
947    /// Returns `Some(StakeAccount)` if found, `None` if not found or tombstoned.
948    pub fn get_stake_account_from_pending(
949        &self,
950        pubkey: &Pubkey,
951    ) -> Option<rialo_stake_cache_interface::StakeAccount> {
952        self.environment_config
953            .stakes_handle
954            .get_stake_account_from_pending(pubkey)
955    }
956
957    /// Check if an epoch rewards initialization request is pending for this block.
958    ///
959    /// Used by DistributeRewards to prevent multiple reward distributions in the same block.
960    /// If this returns true, the DistributeRewards instruction should fail with InvalidArgument.
961    pub fn is_epoch_rewards_init_pending(&self) -> bool {
962        self.environment_config
963            .stakes_handle
964            .is_epoch_rewards_init_pending()
965    }
966
967    /// Get completed frozen epochs (excludes the last/current epoch).
968    ///
969    /// Returns epochs in order from oldest to newest. These are the epochs that have
970    /// completed and are eligible for reward distribution. We exclude the last frozen
971    /// epoch because it represents the current epoch that is still accumulating rewards.
972    pub fn completed_frozen_epochs(&self) -> Vec<u64> {
973        self.environment_config
974            .stakes_handle
975            .completed_frozen_epochs()
976    }
977
978    /// Check if an EpochRewards account exists for the given epoch.
979    ///
980    /// Delegates to the StakesHandle which contains the callback provided by Bank
981    /// to query the StateStore. This allows DistributeRewards to find the first
982    /// completed frozen epoch that doesn't yet have an EpochRewards account.
983    pub fn epoch_rewards_exists(&self, epoch: u64) -> bool {
984        self.environment_config
985            .stakes_handle
986            .epoch_rewards_exists(epoch)
987    }
988
989    /// Check if the previous (most recent) frozen epoch has been adopted by consensus.
990    ///
991    /// Returns `true` if the frozen deque is empty or `frozen.back().consensus_adopted_at`
992    /// is `Some(_)`. Used by the FreezeStakes guard to enforce the no-skip invariant.
993    pub fn is_previous_epoch_adopted(&self) -> bool {
994        self.environment_config
995            .stakes_handle
996            .is_previous_epoch_adopted()
997    }
998
999    /// `(timestamp, consensus_adopted_at)` of the frozen entry for `epoch`.
1000    ///
1001    /// Combines what would otherwise be two separate frozen-deque scans
1002    /// (freeze timestamp + adoption status). See
1003    /// `StakesHandle::get_frozen_epoch_meta` for the semantics of the inner
1004    /// `consensus_adopted_at`.
1005    pub fn get_frozen_epoch_meta(&self, epoch: u64) -> Option<(u64, Option<u64>)> {
1006        self.environment_config
1007            .stakes_handle
1008            .get_frozen_epoch_meta(epoch)
1009    }
1010
1011    /// All stake accounts (baseline + frozen deltas up to and including `epoch`).
1012    pub fn get_all_stake_accounts_from_frozen_epoch(
1013        &self,
1014        epoch: u64,
1015    ) -> Vec<(Pubkey, rialo_stake_cache_interface::StakeAccount)> {
1016        self.environment_config
1017            .stakes_handle
1018            .get_all_stake_accounts_from_frozen_epoch(epoch)
1019    }
1020
1021    /// Signal that the current epoch should be marked as adopted by consensus.
1022    ///
1023    /// **Testing only** — used by `RequestConsensusEpochChange` in testing mode to
1024    /// simulate instant adoption. The signal is deferred and consumed during
1025    /// `Bank::finalize_impl()`, which ensures it runs after the deferred freeze
1026    /// swap — so adoption is always set on the correct `frozen.back()` entry,
1027    /// even when `FreezeStakes(force=true)` and the triggered
1028    /// `RequestConsensusEpochChange` fire in the same block.
1029    #[cfg(feature = "testing")]
1030    pub fn signal_handover(&self, ts: u64) {
1031        self.environment_config.stakes_handle.signal_handover(ts);
1032    }
1033
1034    // Should alignment be enforced during user pointer translation
1035    pub fn get_check_aligned(&self) -> bool {
1036        self.transaction_context
1037            .get_current_instruction_context()
1038            .and_then(|instruction_context| {
1039                let program_account =
1040                    instruction_context.try_borrow_last_program_account(self.transaction_context);
1041                debug_assert!(program_account.is_ok());
1042                program_account
1043            })
1044            .map(|program_account| *program_account.get_owner() != bpf_loader_deprecated::id())
1045            .unwrap_or(true)
1046    }
1047
1048    // Set this instruction syscall context
1049    pub fn set_syscall_context(
1050        &mut self,
1051        syscall_context: SyscallContext,
1052    ) -> Result<(), InstructionError> {
1053        *self
1054            .syscall_context
1055            .last_mut()
1056            .ok_or(InstructionError::CallDepth)? = Some(syscall_context);
1057        Ok(())
1058    }
1059
1060    // Get this instruction's SyscallContext
1061    pub fn get_syscall_context(&self) -> Result<&SyscallContext, InstructionError> {
1062        self.syscall_context
1063            .last()
1064            .and_then(std::option::Option::as_ref)
1065            .ok_or(InstructionError::CallDepth)
1066    }
1067
1068    // Get this instruction's SyscallContext
1069    pub fn get_syscall_context_mut(&mut self) -> Result<&mut SyscallContext, InstructionError> {
1070        self.syscall_context
1071            .last_mut()
1072            .and_then(|syscall_context| syscall_context.as_mut())
1073            .ok_or(InstructionError::CallDepth)
1074    }
1075}
1076
1077#[macro_export]
1078macro_rules! with_mock_invoke_context {
1079    (
1080        $invoke_context:ident,
1081        $transaction_context:ident,
1082        $entry:expr,
1083        $transaction_accounts:expr $(,)?
1084    ) => {
1085        use rialo_s_compute_budget::compute_budget::ComputeBudget;
1086        use rialo_s_log_collector::LogCollector;
1087        use rialo_s_type_overrides::sync::Arc;
1088        use $crate::{
1089            __private::{Hash, ReadableAccount, Rent, TransactionContext},
1090            invoke_context::{EnvironmentConfig, InvokeContext},
1091            loaded_programs::{ProgramCacheEntry, ProgramCacheForTx, ProgramCacheForTxBatch},
1092            sysvar_cache::SysvarCache,
1093        };
1094        let compute_budget = ComputeBudget::default();
1095        let mut $transaction_context = TransactionContext::new(
1096            $transaction_accounts,
1097            Rent::default(),
1098            compute_budget.max_instruction_stack_depth,
1099            compute_budget.max_instruction_trace_length,
1100        );
1101        let mut sysvar_cache = SysvarCache::default();
1102        sysvar_cache.fill_missing_entries(|pubkey, callback| {
1103            for index in 0..$transaction_context.get_number_of_accounts() {
1104                if $transaction_context
1105                    .get_key_of_account_at_index(index)
1106                    .unwrap()
1107                    == pubkey
1108                {
1109                    callback(
1110                        $transaction_context
1111                            .get_account_at_index(index)
1112                            .unwrap()
1113                            .borrow()
1114                            .data(),
1115                    );
1116                }
1117            }
1118        });
1119        // Create a stakes handle with default epoch (0) - tests that need specific epochs
1120        // should set up the stake cache data appropriately.
1121        // The default StakesHandle includes an epoch_rewards_exists_fn that returns false.
1122        let mock_stakes_handle = $crate::invoke_context::StakesHandle::default();
1123        let environment_config = EnvironmentConfig::new(
1124            Hash::default(),
1125            0,
1126            &|_| 0,
1127            Arc::new($crate::active_features::ActiveFeatures::new()),
1128            &sysvar_cache,
1129            0,
1130            mock_stakes_handle,
1131        );
1132        let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default();
1133        if let Some((loader_id, builtin)) = $entry {
1134            program_cache_for_tx_batch.replenish(
1135                loader_id,
1136                Arc::new(ProgramCacheEntry::new_builtin(0, 0, builtin)),
1137            );
1138        }
1139        let mut program_cache_for_tx = ProgramCacheForTx::from_cache(&program_cache_for_tx_batch);
1140        let mut $invoke_context = InvokeContext::new(
1141            &mut $transaction_context,
1142            &mut program_cache_for_tx,
1143            environment_config,
1144            Some(LogCollector::new_ref()),
1145            compute_budget,
1146        );
1147    };
1148    (
1149        $invoke_context:ident,
1150        $transaction_context:ident,
1151        $transaction_accounts:expr $(,)?
1152    ) => {
1153        with_mock_invoke_context!(
1154            $invoke_context,
1155            $transaction_context,
1156            None,
1157            $transaction_accounts
1158        )
1159    };
1160}
1161
1162pub fn mock_process_instruction<
1163    T: Into<StoredAccount>,
1164    F: FnMut(&mut InvokeContext<'_, '_>),
1165    G: FnMut(&mut InvokeContext<'_, '_>),
1166>(
1167    loader_id: &Pubkey,
1168    mut program_indices: Vec<IndexOfAccount>,
1169    instruction_data: &[u8],
1170    transaction_accounts: Vec<(Pubkey, T)>,
1171    instruction_account_metas: Vec<AccountMeta>,
1172    expected_result: Result<(), InstructionError>,
1173    builtin_function: BuiltinFunctionWithContext,
1174    mut pre_adjustments: F,
1175    mut post_adjustments: G,
1176) -> Vec<StoredAccount> {
1177    // Convert to StoredAccount upfront since we need to mutate the vector
1178    let mut transaction_accounts: Vec<TransactionAccount> = transaction_accounts
1179        .into_iter()
1180        .map(|(key, account)| (key, account.into()))
1181        .collect();
1182    let mut instruction_accounts: Vec<InstructionAccount> =
1183        Vec::with_capacity(instruction_account_metas.len());
1184    for (instruction_account_index, account_meta) in instruction_account_metas.iter().enumerate() {
1185        let index_in_transaction = transaction_accounts
1186            .iter()
1187            .position(|(key, _account)| *key == account_meta.pubkey)
1188            .unwrap_or(transaction_accounts.len())
1189            as IndexOfAccount;
1190        let index_in_callee = instruction_accounts
1191            .get(0..instruction_account_index)
1192            .unwrap()
1193            .iter()
1194            .position(|instruction_account| {
1195                instruction_account.index_in_transaction == index_in_transaction
1196            })
1197            .unwrap_or(instruction_account_index) as IndexOfAccount;
1198        instruction_accounts.push(InstructionAccount {
1199            index_in_transaction,
1200            index_in_caller: index_in_transaction,
1201            index_in_callee,
1202            is_signer: account_meta.is_signer,
1203            is_writable: account_meta.is_writable,
1204        });
1205    }
1206    if program_indices.is_empty() {
1207        program_indices.insert(0, transaction_accounts.len() as IndexOfAccount);
1208        let processor_account =
1209            StoredAccount::Data(AccountSharedData::new(0, 0, &native_loader::id()));
1210        transaction_accounts.push((*loader_id, processor_account));
1211    }
1212    let pop_epoch_schedule_account = if !transaction_accounts
1213        .iter()
1214        .any(|(key, _)| *key == sysvar::epoch_schedule::id())
1215    {
1216        transaction_accounts.push((
1217            sysvar::epoch_schedule::id(),
1218            create_account_shared_data_for_test(&EpochSchedule::default()),
1219        ));
1220        true
1221    } else {
1222        false
1223    };
1224    with_mock_invoke_context!(
1225        invoke_context,
1226        transaction_context,
1227        Some((*loader_id, builtin_function)),
1228        transaction_accounts
1229    );
1230    pre_adjustments(&mut invoke_context);
1231    let result = invoke_context.process_instruction(
1232        instruction_data,
1233        &instruction_accounts,
1234        &program_indices,
1235        &mut 0,
1236        &mut ExecuteTimings::default(),
1237    );
1238    assert_eq!(result, expected_result);
1239    post_adjustments(&mut invoke_context);
1240    drop(invoke_context);
1241    let mut transaction_accounts = transaction_context.deconstruct_without_keys().unwrap();
1242    if pop_epoch_schedule_account {
1243        transaction_accounts.pop();
1244    }
1245    transaction_accounts.pop();
1246    transaction_accounts
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251    use rialo_s_compute_budget::compute_budget_limits;
1252    use rialo_s_instruction::Instruction;
1253    use rialo_s_rent::Rent;
1254    use serde::{Deserialize, Serialize};
1255
1256    use super::*;
1257
1258    #[derive(Debug, Serialize, Deserialize)]
1259    enum MockInstruction {
1260        NoopSuccess,
1261        NoopFail,
1262        ModifyOwned,
1263        ModifyNotOwned,
1264        ModifyReadonly,
1265        UnbalancedPush,
1266        UnbalancedPop,
1267        ConsumeComputeUnits {
1268            compute_units_to_consume: u64,
1269            desired_result: Result<(), InstructionError>,
1270        },
1271        Resize {
1272            new_len: u64,
1273        },
1274    }
1275
1276    const MOCK_BUILTIN_COMPUTE_UNIT_COST: u64 = 1;
1277
1278    declare_process_instruction!(
1279        MockBuiltin,
1280        MOCK_BUILTIN_COMPUTE_UNIT_COST,
1281        |invoke_context| {
1282            let transaction_context = &invoke_context.transaction_context;
1283            let instruction_context = transaction_context.get_current_instruction_context()?;
1284            let instruction_data = instruction_context.get_instruction_data();
1285            let program_id = instruction_context.get_last_program_key(transaction_context)?;
1286            let instruction_accounts = (0..4)
1287                .map(|instruction_account_index| InstructionAccount {
1288                    index_in_transaction: instruction_account_index,
1289                    index_in_caller: instruction_account_index,
1290                    index_in_callee: instruction_account_index,
1291                    is_signer: false,
1292                    is_writable: false,
1293                })
1294                .collect::<Vec<_>>();
1295            assert_eq!(
1296                program_id,
1297                instruction_context
1298                    .try_borrow_instruction_account(transaction_context, 0)?
1299                    .get_owner()
1300            );
1301            assert_ne!(
1302                instruction_context
1303                    .try_borrow_instruction_account(transaction_context, 1)?
1304                    .get_owner(),
1305                instruction_context
1306                    .try_borrow_instruction_account(transaction_context, 0)?
1307                    .get_key()
1308            );
1309
1310            if let Ok(instruction) = bincode::deserialize(instruction_data) {
1311                match instruction {
1312                    MockInstruction::NoopSuccess => (),
1313                    MockInstruction::NoopFail => return Err(InstructionError::GenericError),
1314                    MockInstruction::ModifyOwned => instruction_context
1315                        .try_borrow_instruction_account(transaction_context, 0)?
1316                        .set_data_from_slice(&[1])?,
1317                    MockInstruction::ModifyNotOwned => instruction_context
1318                        .try_borrow_instruction_account(transaction_context, 1)?
1319                        .set_data_from_slice(&[1])?,
1320                    MockInstruction::ModifyReadonly => instruction_context
1321                        .try_borrow_instruction_account(transaction_context, 2)?
1322                        .set_data_from_slice(&[1])?,
1323                    MockInstruction::UnbalancedPush => {
1324                        instruction_context
1325                            .try_borrow_instruction_account(transaction_context, 0)?
1326                            .checked_add_kelvins(1)?;
1327                        let program_id = *transaction_context.get_key_of_account_at_index(3)?;
1328                        let metas = vec![
1329                            AccountMeta::new_readonly(
1330                                *transaction_context.get_key_of_account_at_index(0)?,
1331                                false,
1332                            ),
1333                            AccountMeta::new_readonly(
1334                                *transaction_context.get_key_of_account_at_index(1)?,
1335                                false,
1336                            ),
1337                        ];
1338                        let inner_instruction = Instruction::new_with_bincode(
1339                            program_id,
1340                            &MockInstruction::NoopSuccess,
1341                            metas,
1342                        );
1343                        invoke_context
1344                            .transaction_context
1345                            .get_next_instruction_context()
1346                            .unwrap()
1347                            .configure(&[3], &instruction_accounts, &[]);
1348                        let result = invoke_context.push();
1349                        assert_eq!(result, Err(InstructionError::UnbalancedInstruction));
1350                        result?;
1351                        invoke_context
1352                            .native_invoke(inner_instruction.into(), &[])
1353                            .and(invoke_context.pop())?;
1354                    }
1355                    MockInstruction::UnbalancedPop => instruction_context
1356                        .try_borrow_instruction_account(transaction_context, 0)?
1357                        .checked_add_kelvins(1)?,
1358                    MockInstruction::ConsumeComputeUnits {
1359                        compute_units_to_consume,
1360                        desired_result,
1361                    } => {
1362                        invoke_context
1363                            .consume_checked(compute_units_to_consume)
1364                            .map_err(|_| InstructionError::ComputationalBudgetExceeded)?;
1365                        return desired_result;
1366                    }
1367                    MockInstruction::Resize { new_len } => instruction_context
1368                        .try_borrow_instruction_account(transaction_context, 0)?
1369                        .set_data(vec![0; new_len as usize])?,
1370                }
1371            } else {
1372                return Err(InstructionError::InvalidInstructionData);
1373            }
1374            Ok(())
1375        }
1376    );
1377
1378    #[test]
1379    fn test_instruction_stack_height() {
1380        let one_more_than_max_depth = ComputeBudget::default()
1381            .max_instruction_stack_depth
1382            .saturating_add(1);
1383        let mut invoke_stack = vec![];
1384        let mut transaction_accounts = vec![];
1385        let mut instruction_accounts = vec![];
1386        for index in 0..one_more_than_max_depth {
1387            invoke_stack.push(rialo_s_pubkey::new_rand());
1388            transaction_accounts.push((
1389                rialo_s_pubkey::new_rand(),
1390                AccountSharedData::new(index as u64, 1, invoke_stack.get(index).unwrap()),
1391            ));
1392            instruction_accounts.push(InstructionAccount {
1393                index_in_transaction: index as IndexOfAccount,
1394                index_in_caller: index as IndexOfAccount,
1395                index_in_callee: instruction_accounts.len() as IndexOfAccount,
1396                is_signer: false,
1397                is_writable: true,
1398            });
1399        }
1400        for (index, program_id) in invoke_stack.iter().enumerate() {
1401            transaction_accounts.push((
1402                *program_id,
1403                AccountSharedData::new(1, 1, &rialo_s_pubkey::Pubkey::default()),
1404            ));
1405            instruction_accounts.push(InstructionAccount {
1406                index_in_transaction: index as IndexOfAccount,
1407                index_in_caller: index as IndexOfAccount,
1408                index_in_callee: index as IndexOfAccount,
1409                is_signer: false,
1410                is_writable: false,
1411            });
1412        }
1413        let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
1414            .into_iter()
1415            .map(|(k, v)| (k, v.into()))
1416            .collect();
1417        with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
1418
1419        // Check call depth increases and has a limit
1420        let mut depth_reached = 0;
1421        for _ in 0..invoke_stack.len() {
1422            invoke_context
1423                .transaction_context
1424                .get_next_instruction_context()
1425                .unwrap()
1426                .configure(
1427                    &[one_more_than_max_depth.saturating_add(depth_reached) as IndexOfAccount],
1428                    &instruction_accounts,
1429                    &[],
1430                );
1431            if Err(InstructionError::CallDepth) == invoke_context.push() {
1432                break;
1433            }
1434            depth_reached = depth_reached.saturating_add(1);
1435        }
1436        assert_ne!(depth_reached, 0);
1437        assert!(depth_reached < one_more_than_max_depth);
1438    }
1439
1440    #[test]
1441    fn test_max_instruction_trace_length() {
1442        const MAX_INSTRUCTIONS: usize = 8;
1443        let mut transaction_context = TransactionContext::new::<StoredAccount>(
1444            Vec::new(),
1445            Rent::default(),
1446            1,
1447            MAX_INSTRUCTIONS,
1448        );
1449        for _ in 0..MAX_INSTRUCTIONS {
1450            transaction_context.push().unwrap();
1451            transaction_context.pop().unwrap();
1452        }
1453        assert_eq!(
1454            transaction_context.push(),
1455            Err(InstructionError::MaxInstructionTraceLengthExceeded)
1456        );
1457    }
1458
1459    #[test]
1460    fn test_process_instruction() {
1461        let callee_program_id = rialo_s_pubkey::new_rand();
1462        let owned_account = AccountSharedData::new(42, 1, &callee_program_id);
1463        let not_owned_account = AccountSharedData::new(84, 1, &rialo_s_pubkey::new_rand());
1464        let readonly_account = AccountSharedData::new(168, 1, &rialo_s_pubkey::new_rand());
1465        let loader_account = AccountSharedData::new(0, 1, &native_loader::id());
1466        let program_account = AccountSharedData::new(1, 1, &native_loader::id());
1467        let transaction_accounts = vec![
1468            (rialo_s_pubkey::new_rand(), owned_account),
1469            (rialo_s_pubkey::new_rand(), not_owned_account),
1470            (rialo_s_pubkey::new_rand(), readonly_account),
1471            (callee_program_id, program_account),
1472            (rialo_s_pubkey::new_rand(), loader_account),
1473        ];
1474        let metas = vec![
1475            AccountMeta::new(transaction_accounts.first().unwrap().0, false),
1476            AccountMeta::new(transaction_accounts.get(1).unwrap().0, false),
1477            AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false),
1478        ];
1479        let instruction_accounts = (0..4)
1480            .map(|instruction_account_index| InstructionAccount {
1481                index_in_transaction: instruction_account_index,
1482                index_in_caller: instruction_account_index,
1483                index_in_callee: instruction_account_index,
1484                is_signer: false,
1485                is_writable: instruction_account_index < 2,
1486            })
1487            .collect::<Vec<_>>();
1488        let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
1489            .into_iter()
1490            .map(|(k, v)| (k, v.into()))
1491            .collect();
1492        with_mock_invoke_context!(
1493            invoke_context,
1494            transaction_context,
1495            Some((callee_program_id, MockBuiltin::vm)),
1496            transaction_accounts
1497        );
1498
1499        // Account modification tests
1500        let cases = vec![
1501            (MockInstruction::NoopSuccess, Ok(())),
1502            (
1503                MockInstruction::NoopFail,
1504                Err(InstructionError::GenericError),
1505            ),
1506            (MockInstruction::ModifyOwned, Ok(())),
1507            (
1508                MockInstruction::ModifyNotOwned,
1509                Err(InstructionError::ExternalAccountDataModified),
1510            ),
1511            (
1512                MockInstruction::ModifyReadonly,
1513                Err(InstructionError::ReadonlyDataModified),
1514            ),
1515            (
1516                MockInstruction::UnbalancedPush,
1517                Err(InstructionError::UnbalancedInstruction),
1518            ),
1519            (
1520                MockInstruction::UnbalancedPop,
1521                Err(InstructionError::UnbalancedInstruction),
1522            ),
1523        ];
1524        for case in cases {
1525            invoke_context
1526                .transaction_context
1527                .get_next_instruction_context()
1528                .unwrap()
1529                .configure(&[4], &instruction_accounts, &[]);
1530            invoke_context.push().unwrap();
1531            let inner_instruction =
1532                Instruction::new_with_bincode(callee_program_id, &case.0, metas.clone());
1533            let result = invoke_context
1534                .native_invoke(inner_instruction.into(), &[])
1535                .and(invoke_context.pop());
1536            assert_eq!(result, case.1);
1537        }
1538
1539        // Compute unit consumption tests
1540        let compute_units_to_consume = 10;
1541        let expected_results = vec![Ok(()), Err(InstructionError::GenericError)];
1542        for expected_result in expected_results {
1543            invoke_context
1544                .transaction_context
1545                .get_next_instruction_context()
1546                .unwrap()
1547                .configure(&[4], &instruction_accounts, &[]);
1548            invoke_context.push().unwrap();
1549            let inner_instruction = Instruction::new_with_bincode(
1550                callee_program_id,
1551                &MockInstruction::ConsumeComputeUnits {
1552                    compute_units_to_consume,
1553                    desired_result: expected_result.clone(),
1554                },
1555                metas.clone(),
1556            );
1557            let inner_instruction = StableInstruction::from(inner_instruction);
1558            let (inner_instruction_accounts, program_indices) = invoke_context
1559                .prepare_instruction(&inner_instruction, &[])
1560                .unwrap();
1561
1562            let mut compute_units_consumed = 0;
1563            let result = invoke_context.process_instruction(
1564                &inner_instruction.data,
1565                &inner_instruction_accounts,
1566                &program_indices,
1567                &mut compute_units_consumed,
1568                &mut ExecuteTimings::default(),
1569            );
1570
1571            // Because the instruction had compute cost > 0, then regardless of the execution result,
1572            // the number of compute units consumed should be a non-default which is something greater
1573            // than zero.
1574            assert!(compute_units_consumed > 0);
1575            assert_eq!(
1576                compute_units_consumed,
1577                compute_units_to_consume.saturating_add(MOCK_BUILTIN_COMPUTE_UNIT_COST),
1578            );
1579            assert_eq!(result, expected_result);
1580
1581            invoke_context.pop().unwrap();
1582        }
1583    }
1584
1585    #[test]
1586    fn test_invoke_context_compute_budget() {
1587        let transaction_accounts = vec![(rialo_s_pubkey::new_rand(), AccountSharedData::default())];
1588        let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
1589            .into_iter()
1590            .map(|(k, v)| (k, v.into()))
1591            .collect();
1592
1593        with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
1594        invoke_context.compute_budget = ComputeBudget::new(
1595            compute_budget_limits::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
1596        );
1597
1598        invoke_context
1599            .transaction_context
1600            .get_next_instruction_context()
1601            .unwrap()
1602            .configure(&[0], &[], &[]);
1603        invoke_context.push().unwrap();
1604        assert_eq!(
1605            *invoke_context.get_compute_budget(),
1606            ComputeBudget::new(
1607                compute_budget_limits::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64
1608            )
1609        );
1610        invoke_context.pop().unwrap();
1611    }
1612
1613    #[test]
1614    fn test_process_instruction_accounts_resize_delta() {
1615        let program_key = Pubkey::new_unique();
1616        let user_account_data_len = 123u64;
1617        let user_account =
1618            AccountSharedData::new(100, user_account_data_len as usize, &program_key);
1619        let dummy_account = AccountSharedData::new(10, 0, &program_key);
1620        let program_account = AccountSharedData::new(500, 500, &native_loader::id());
1621        let transaction_accounts = vec![
1622            (Pubkey::new_unique(), user_account),
1623            (Pubkey::new_unique(), dummy_account),
1624            (program_key, program_account),
1625        ];
1626        let instruction_accounts = [
1627            InstructionAccount {
1628                index_in_transaction: 0,
1629                index_in_caller: 0,
1630                index_in_callee: 0,
1631                is_signer: false,
1632                is_writable: true,
1633            },
1634            InstructionAccount {
1635                index_in_transaction: 1,
1636                index_in_caller: 1,
1637                index_in_callee: 1,
1638                is_signer: false,
1639                is_writable: false,
1640            },
1641        ];
1642        let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
1643            .into_iter()
1644            .map(|(k, v)| (k, v.into()))
1645            .collect();
1646        with_mock_invoke_context!(
1647            invoke_context,
1648            transaction_context,
1649            Some((program_key, MockBuiltin::vm)),
1650            transaction_accounts
1651        );
1652
1653        // Test: Resize the account to *the same size*, so not consuming any additional size; this must succeed
1654        {
1655            let resize_delta: i64 = 0;
1656            let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
1657            let instruction_data =
1658                bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
1659
1660            let result = invoke_context.process_instruction(
1661                &instruction_data,
1662                &instruction_accounts,
1663                &[2],
1664                &mut 0,
1665                &mut ExecuteTimings::default(),
1666            );
1667
1668            assert!(result.is_ok());
1669            assert_eq!(
1670                invoke_context
1671                    .transaction_context
1672                    .accounts_resize_delta()
1673                    .unwrap(),
1674                resize_delta
1675            );
1676        }
1677
1678        // Test: Resize the account larger; this must succeed
1679        {
1680            let resize_delta: i64 = 1;
1681            let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
1682            let instruction_data =
1683                bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
1684
1685            let result = invoke_context.process_instruction(
1686                &instruction_data,
1687                &instruction_accounts,
1688                &[2],
1689                &mut 0,
1690                &mut ExecuteTimings::default(),
1691            );
1692
1693            assert!(result.is_ok());
1694            assert_eq!(
1695                invoke_context
1696                    .transaction_context
1697                    .accounts_resize_delta()
1698                    .unwrap(),
1699                resize_delta
1700            );
1701        }
1702
1703        // Test: Resize the account smaller; this must succeed
1704        {
1705            let resize_delta: i64 = -1;
1706            let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
1707            let instruction_data =
1708                bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
1709
1710            let result = invoke_context.process_instruction(
1711                &instruction_data,
1712                &instruction_accounts,
1713                &[2],
1714                &mut 0,
1715                &mut ExecuteTimings::default(),
1716            );
1717
1718            assert!(result.is_ok());
1719            assert_eq!(
1720                invoke_context
1721                    .transaction_context
1722                    .accounts_resize_delta()
1723                    .unwrap(),
1724                resize_delta
1725            );
1726        }
1727    }
1728}