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