use std::{
alloc::Layout,
cell::RefCell,
collections::HashSet,
fmt::{self, Debug},
rc::Rc,
};
use rialo_hash::Hash;
use rialo_s_account::{create_account_shared_data_for_test, AccountSharedData, StoredAccount};
use rialo_s_clock::Slot;
use rialo_s_compute_budget::compute_budget::ComputeBudget;
use rialo_s_epoch_schedule::EpochSchedule;
use rialo_s_feature_set::{
lift_cpi_caller_restriction, move_precompile_verification_to_svm,
remove_accounts_executable_flag_checks, FeatureSet,
};
use rialo_s_instruction::{error::InstructionError, AccountMeta};
use rialo_s_log_collector::{ic_msg, LogCollector};
use rialo_s_measure::measure::Measure;
use rialo_s_precompiles::Precompile;
use rialo_s_pubkey::Pubkey;
use rialo_s_sdk_ids::{bpf_loader_deprecated, native_loader, sysvar};
use rialo_s_stable_layout::stable_instruction::StableInstruction;
use rialo_s_timings::{ExecuteDetailsTimings, ExecuteTimings};
use rialo_s_transaction_context::{
IndexOfAccount, InstructionAccount, TransactionAccount, TransactionContext,
};
use rialo_s_type_overrides::sync::{atomic::Ordering, Arc};
pub use rialo_stake_cache_interface::{StakeCacheData, StakesHandle, ValidatorAccount};
use solana_sbpf::{
ebpf::MM_HEAP_START,
error::{EbpfError, ProgramResult},
memory_region::MemoryMapping,
program::{BuiltinFunction, SBPFVersion},
vm::{Config, ContextObject, EbpfVm},
};
use crate::{
loaded_programs::{ProgramCacheEntryType, ProgramCacheForTx, ProgramRuntimeEnvironments},
stable_log,
sysvar_cache::SysvarCache,
};
pub type BuiltinFunctionWithContext = BuiltinFunction<InvokeContext<'static, 'static>>;
#[macro_export]
macro_rules! declare_process_instruction {
($process_instruction:ident, $cu_to_consume:expr, |$invoke_context:ident| $inner:tt) => {
$crate::solana_sbpf::declare_builtin_function!(
$process_instruction,
fn rust(
invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>,
_arg0: u64,
_arg1: u64,
_arg2: u64,
_arg3: u64,
_arg4: u64,
_memory_mapping: &mut $crate::solana_sbpf::memory_region::MemoryMapping<'_>,
) -> std::result::Result<u64, Box<dyn std::error::Error>> {
fn process_instruction_inner(
$invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>,
) -> std::result::Result<(), $crate::__private::InstructionError>
$inner
let consumption_result = if $cu_to_consume > 0
{
invoke_context.consume_checked($cu_to_consume)
} else {
Ok(())
};
consumption_result
.and_then(|_| {
process_instruction_inner(invoke_context)
.map(|_| 0)
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
})
.into()
}
);
};
}
pub enum AccountInsertError {
TooManyAccounts,
AccountAlreadyLoaded,
}
#[allow(clippy::result_unit_err)]
pub trait RuntimeAccountLoader {
fn load_account(&self, pubkey: &Pubkey) -> Result<Option<StoredAccount>, ()>;
}
impl ContextObject for InvokeContext<'_, '_> {
fn trace(&mut self, state: [u64; 12]) {
self.syscall_context
.last_mut()
.unwrap()
.as_mut()
.unwrap()
.trace_log
.push(state);
}
fn consume(&mut self, amount: u64) {
let mut compute_meter = self.compute_meter.borrow_mut();
*compute_meter = compute_meter.saturating_sub(amount);
}
fn get_remaining(&self) -> u64 {
*self.compute_meter.borrow()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AllocErr;
impl fmt::Display for AllocErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Error: Memory allocation failed")
}
}
pub struct BpfAllocator {
len: u64,
pos: u64,
}
impl BpfAllocator {
pub fn new(len: u64) -> Self {
Self { len, pos: 0 }
}
pub fn alloc(&mut self, layout: Layout) -> Result<u64, AllocErr> {
let bytes_to_align = (self.pos as *const u8).align_offset(layout.align()) as u64;
if self
.pos
.saturating_add(bytes_to_align)
.saturating_add(layout.size() as u64)
<= self.len
{
self.pos = self.pos.saturating_add(bytes_to_align);
let addr = MM_HEAP_START.saturating_add(self.pos);
self.pos = self.pos.saturating_add(layout.size() as u64);
Ok(addr)
} else {
Err(AllocErr)
}
}
}
pub struct EnvironmentConfig<'a> {
pub blockhash: Hash,
pub blockhash_kelvins_per_signature: u64,
get_epoch_vote_account_stake_callback: &'a dyn Fn(&'a Pubkey) -> u64,
pub feature_set: Arc<FeatureSet>,
sysvar_cache: &'a SysvarCache,
pub random_seed: u64,
stakes_handle: StakesHandle,
}
impl<'a> EnvironmentConfig<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
blockhash: Hash,
blockhash_kelvins_per_signature: u64,
get_epoch_vote_account_stake_callback: &'a dyn Fn(&'a Pubkey) -> u64,
feature_set: Arc<FeatureSet>,
sysvar_cache: &'a SysvarCache,
random_seed: u64,
stakes_handle: StakesHandle,
) -> Self {
Self {
blockhash,
blockhash_kelvins_per_signature,
get_epoch_vote_account_stake_callback,
feature_set,
sysvar_cache,
random_seed,
stakes_handle,
}
}
}
pub struct SyscallContext {
pub allocator: BpfAllocator,
pub accounts_metadata: Vec<SerializedAccountMetadata>,
pub trace_log: Vec<[u64; 12]>,
}
#[derive(Debug, Clone)]
pub struct SerializedAccountMetadata {
pub original_data_len: usize,
pub vm_data_addr: u64,
pub vm_key_addr: u64,
pub vm_kelvins_addr: u64,
pub vm_owner_addr: u64,
}
pub struct InvokeContext<'a, 'b> {
pub transaction_context: &'a mut TransactionContext,
pub program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
pub environment_config: EnvironmentConfig<'a>,
compute_budget: ComputeBudget,
compute_meter: RefCell<u64>,
log_collector: Option<Rc<RefCell<LogCollector>>>,
pub execute_time: Option<Measure>,
pub timings: ExecuteDetailsTimings,
pub syscall_context: Vec<Option<SyscallContext>>,
traces: Vec<Vec<[u64; 12]>>,
pub account_loader: Option<Box<dyn RuntimeAccountLoader + 'a>>,
loaded_accounts: HashSet<Pubkey>,
num_account_loads: usize,
}
impl<'a, 'b> InvokeContext<'a, 'b> {
#[allow(clippy::too_many_arguments)]
pub fn new(
transaction_context: &'a mut TransactionContext,
program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
environment_config: EnvironmentConfig<'a>,
log_collector: Option<Rc<RefCell<LogCollector>>>,
compute_budget: ComputeBudget,
) -> Self {
let loaded_accounts = transaction_context.account_keys().copied().collect();
Self {
transaction_context,
program_cache_for_tx_batch,
environment_config,
log_collector,
compute_budget,
compute_meter: RefCell::new(compute_budget.compute_unit_limit),
execute_time: None,
timings: ExecuteDetailsTimings::default(),
syscall_context: Vec::new(),
traces: Vec::new(),
account_loader: None,
loaded_accounts,
num_account_loads: 0usize,
}
}
pub fn new_with_account_loader(
transaction_context: &'a mut TransactionContext,
program_cache_for_tx_batch: &'a mut ProgramCacheForTx<'b>,
environment_config: EnvironmentConfig<'a>,
log_collector: Option<Rc<RefCell<LogCollector>>>,
compute_budget: ComputeBudget,
account_loader: Box<dyn RuntimeAccountLoader + 'a>,
num_account_locks: usize,
num_writable_accounts: usize,
) -> Self {
let loaded_accounts = transaction_context.account_keys().copied().collect();
Self {
transaction_context,
program_cache_for_tx_batch,
environment_config,
log_collector,
compute_budget,
compute_meter: RefCell::new(compute_budget.compute_unit_limit),
execute_time: None,
timings: ExecuteDetailsTimings::default(),
syscall_context: Vec::new(),
traces: Vec::new(),
account_loader: Some(account_loader),
loaded_accounts,
num_account_loads: num_account_locks.saturating_sub(num_writable_accounts),
}
}
pub fn add_loaded_account(&mut self, pubkey: Pubkey) -> Result<(), AccountInsertError> {
match self.num_account_loads.checked_sub(1) {
Some(value) => self.num_account_loads = value,
None => return Err(AccountInsertError::TooManyAccounts),
}
if self.loaded_accounts.insert(pubkey) {
return Ok(());
}
Err(AccountInsertError::AccountAlreadyLoaded)
}
pub fn get_environments_for_slot(
&self,
effective_slot: Slot,
) -> Result<&ProgramRuntimeEnvironments, InstructionError> {
let epoch_schedule = self.environment_config.sysvar_cache.get_epoch_schedule()?;
let epoch = epoch_schedule.get_epoch(effective_slot);
Ok(self
.program_cache_for_tx_batch
.get_environments_for_epoch(epoch))
}
pub fn push(&mut self) -> Result<(), InstructionError> {
let instruction_context = self
.transaction_context
.get_instruction_context_at_index_in_trace(
self.transaction_context.get_instruction_trace_length(),
)?;
let program_id = instruction_context
.get_last_program_key(self.transaction_context)
.map_err(|_| InstructionError::UnsupportedProgramId)?;
if self
.transaction_context
.get_instruction_context_stack_height()
!= 0
{
let contains = (0..self
.transaction_context
.get_instruction_context_stack_height())
.any(|level| {
self.transaction_context
.get_instruction_context_at_nesting_level(level)
.and_then(|instruction_context| {
instruction_context
.try_borrow_last_program_account(self.transaction_context)
})
.map(|program_account| program_account.get_key() == program_id)
.unwrap_or(false)
});
let is_last = self
.transaction_context
.get_current_instruction_context()
.and_then(|instruction_context| {
instruction_context.try_borrow_last_program_account(self.transaction_context)
})
.map(|program_account| program_account.get_key() == program_id)
.unwrap_or(false);
if contains && !is_last {
return Err(InstructionError::ReentrancyNotAllowed);
}
}
self.syscall_context.push(None);
self.transaction_context.push()
}
fn pop(&mut self) -> Result<(), InstructionError> {
if let Some(Some(syscall_context)) = self.syscall_context.pop() {
self.traces.push(syscall_context.trace_log);
}
self.transaction_context.pop()
}
pub fn get_stack_height(&self) -> usize {
self.transaction_context
.get_instruction_context_stack_height()
}
pub fn native_invoke(
&mut self,
instruction: StableInstruction,
signers: &[Pubkey],
) -> Result<(), InstructionError> {
let (instruction_accounts, program_indices) =
self.prepare_instruction(&instruction, signers)?;
let mut compute_units_consumed = 0;
self.process_instruction(
&instruction.data,
&instruction_accounts,
&program_indices,
&mut compute_units_consumed,
&mut ExecuteTimings::default(),
)?;
Ok(())
}
#[allow(clippy::type_complexity)]
pub fn prepare_instruction(
&mut self,
instruction: &StableInstruction,
signers: &[Pubkey],
) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
self.prepare_instruction_inner(instruction.program_id, &instruction.accounts, signers)
}
pub fn prepare_cpi_instruction(
&mut self,
program_id: Pubkey,
account_metas: &[AccountMeta],
signers: &[Pubkey],
) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
self.prepare_instruction_inner(program_id, account_metas, signers)
}
pub fn prepare_instruction_inner(
&mut self,
callee_program_id: Pubkey,
account_metas: &[AccountMeta],
signers: &[Pubkey],
) -> Result<(Vec<InstructionAccount>, Vec<IndexOfAccount>), InstructionError> {
let instruction_context = self.transaction_context.get_current_instruction_context()?;
let mut deduplicated_instruction_accounts: Vec<InstructionAccount> = Vec::new();
let mut duplicate_indicies = Vec::with_capacity(account_metas.len());
for (instruction_account_index, account_meta) in account_metas.iter().enumerate() {
let index_in_transaction = self
.transaction_context
.find_index_of_account(&account_meta.pubkey)
.ok_or_else(|| {
ic_msg!(
self,
"Instruction references an unknown account {}",
account_meta.pubkey,
);
InstructionError::MissingAccount
})?;
if let Some(duplicate_index) =
deduplicated_instruction_accounts
.iter()
.position(|instruction_account| {
instruction_account.index_in_transaction == index_in_transaction
})
{
duplicate_indicies.push(duplicate_index);
let instruction_account = deduplicated_instruction_accounts
.get_mut(duplicate_index)
.ok_or(InstructionError::NotEnoughAccountKeys)?;
instruction_account.is_signer |= account_meta.is_signer;
instruction_account.is_writable |= account_meta.is_writable;
} else {
let index_in_caller = instruction_context
.find_index_of_instruction_account(
self.transaction_context,
&account_meta.pubkey,
)
.ok_or_else(|| {
ic_msg!(
self,
"Instruction references an unknown account {}",
account_meta.pubkey,
);
InstructionError::MissingAccount
})?;
duplicate_indicies.push(deduplicated_instruction_accounts.len());
deduplicated_instruction_accounts.push(InstructionAccount {
index_in_transaction,
index_in_caller,
index_in_callee: instruction_account_index as IndexOfAccount,
is_signer: account_meta.is_signer,
is_writable: account_meta.is_writable,
});
}
}
for instruction_account in deduplicated_instruction_accounts.iter() {
let borrowed_account = instruction_context.try_borrow_instruction_account(
self.transaction_context,
instruction_account.index_in_caller,
)?;
if instruction_account.is_writable && !borrowed_account.is_writable() {
ic_msg!(
self,
"{}'s writable privilege escalated",
borrowed_account.get_key(),
);
return Err(InstructionError::PrivilegeEscalation);
}
if instruction_account.is_signer
&& !(borrowed_account.is_signer() || signers.contains(borrowed_account.get_key()))
{
ic_msg!(
self,
"{}'s signer privilege escalated",
borrowed_account.get_key()
);
return Err(InstructionError::PrivilegeEscalation);
}
}
let instruction_accounts = duplicate_indicies
.into_iter()
.map(|duplicate_index| {
deduplicated_instruction_accounts
.get(duplicate_index)
.cloned()
.ok_or(InstructionError::NotEnoughAccountKeys)
})
.collect::<Result<Vec<InstructionAccount>, InstructionError>>()?;
let program_account_index = if self
.get_feature_set()
.is_active(&lift_cpi_caller_restriction::id())
{
match (
self.transaction_context
.find_index_of_program_account(&callee_program_id),
&self.account_loader,
) {
(Some(index), _) => index,
(None, Some(account_loader)) => {
match account_loader.load_account(&callee_program_id) {
Ok(Some(account)) => {
self.transaction_context
.add_account((callee_program_id, account.clone()));
assert!(self.loaded_accounts.insert(callee_program_id));
self.transaction_context
.find_index_of_program_account(&callee_program_id)
.expect("program to exist")
}
Ok(None) => return Err(InstructionError::MissingAccount),
Err(()) => return Err(InstructionError::GenericError),
}
}
(None, None) => {
ic_msg!(self, "Unknown program {}", callee_program_id);
return Err(InstructionError::MissingAccount);
}
}
} else {
let program_account_index = instruction_context
.find_index_of_instruction_account(self.transaction_context, &callee_program_id)
.ok_or_else(|| {
ic_msg!(self, "Unknown program {}", callee_program_id);
InstructionError::MissingAccount
})?;
let borrowed_program_account = instruction_context
.try_borrow_instruction_account(self.transaction_context, program_account_index)?;
#[allow(deprecated)]
if !self
.get_feature_set()
.is_active(&remove_accounts_executable_flag_checks::id())
&& !borrowed_program_account.is_executable()
{
ic_msg!(self, "Account {} is not executable", callee_program_id);
return Err(InstructionError::AccountNotExecutable);
}
borrowed_program_account.get_index_in_transaction()
};
Ok((instruction_accounts, vec![program_account_index]))
}
pub fn process_instruction(
&mut self,
instruction_data: &[u8],
instruction_accounts: &[InstructionAccount],
program_indices: &[IndexOfAccount],
compute_units_consumed: &mut u64,
timings: &mut ExecuteTimings,
) -> Result<(), InstructionError> {
*compute_units_consumed = 0;
self.transaction_context
.get_next_instruction_context()?
.configure(program_indices, instruction_accounts, instruction_data);
self.push()?;
self.process_executable_chain(compute_units_consumed, timings)
.and(self.pop())
}
pub fn process_precompile<'ix_data>(
&mut self,
precompile: &Precompile,
instruction_data: &[u8],
instruction_accounts: &[InstructionAccount],
program_indices: &[IndexOfAccount],
message_instruction_datas_iter: impl Iterator<Item = &'ix_data [u8]>,
) -> Result<(), InstructionError> {
self.transaction_context
.get_next_instruction_context()?
.configure(program_indices, instruction_accounts, instruction_data);
self.push()?;
let feature_set = self.get_feature_set();
let move_precompile_verification_to_svm =
feature_set.is_active(&move_precompile_verification_to_svm::id());
if move_precompile_verification_to_svm {
let instruction_datas: Vec<_> = message_instruction_datas_iter.collect();
precompile
.verify(instruction_data, &instruction_datas, feature_set)
.map_err(InstructionError::from)
.and(self.pop())
} else {
self.pop()
}
}
fn process_executable_chain(
&mut self,
compute_units_consumed: &mut u64,
timings: &mut ExecuteTimings,
) -> Result<(), InstructionError> {
let instruction_context = self.transaction_context.get_current_instruction_context()?;
let process_executable_chain_time = Measure::start("process_executable_chain_time");
let builtin_id = {
debug_assert!(instruction_context.get_number_of_program_accounts() <= 1);
let borrowed_root_account = instruction_context
.try_borrow_program_account(self.transaction_context, 0)
.map_err(|_| InstructionError::UnsupportedProgramId)?;
let owner_id = borrowed_root_account.get_owner();
if native_loader::check_id(owner_id) {
*borrowed_root_account.get_key()
} else {
*owner_id
}
};
const ENTRYPOINT_KEY: u32 = 0x71E3CF81;
let entry = self
.program_cache_for_tx_batch
.find(&builtin_id)
.ok_or(InstructionError::UnsupportedProgramId)?;
let function = match &entry.program {
ProgramCacheEntryType::Builtin(program) => program
.get_function_registry()
.lookup_by_key(ENTRYPOINT_KEY)
.map(|(_name, function)| function),
_ => None,
}
.ok_or(InstructionError::UnsupportedProgramId)?;
entry.ix_usage_counter.fetch_add(1, Ordering::Relaxed);
let program_id = *instruction_context.get_last_program_key(self.transaction_context)?;
self.transaction_context
.set_return_data(program_id, Vec::new())?;
let logger = self.get_log_collector();
stable_log::program_invoke(&logger, &program_id, self.get_stack_height());
let pre_remaining_units = self.get_remaining();
let mock_config = Config::default();
let empty_memory_mapping =
MemoryMapping::new(Vec::new(), &mock_config, SBPFVersion::V0).unwrap();
let mut vm = EbpfVm::new(
self.program_cache_for_tx_batch
.environments
.program_runtime_v2
.clone(),
SBPFVersion::V0,
unsafe {
std::mem::transmute::<&mut InvokeContext<'_, '_>, &mut InvokeContext<'_, '_>>(self)
},
empty_memory_mapping,
0,
);
vm.invoke_function(function);
let result = match vm.program_result {
ProgramResult::Ok(_) => {
stable_log::program_success(&logger, &program_id);
Ok(())
}
ProgramResult::Err(ref err) => {
if let EbpfError::SyscallError(syscall_error) = err {
if let Some(instruction_err) = syscall_error.downcast_ref::<InstructionError>()
{
stable_log::program_failure(&logger, &program_id, instruction_err);
Err(instruction_err.clone())
} else {
stable_log::program_failure(&logger, &program_id, syscall_error);
Err(InstructionError::ProgramFailedToComplete)
}
} else {
stable_log::program_failure(&logger, &program_id, err);
Err(InstructionError::ProgramFailedToComplete)
}
}
};
let post_remaining_units = self.get_remaining();
*compute_units_consumed = pre_remaining_units.saturating_sub(post_remaining_units);
if builtin_id == program_id && result.is_ok() && *compute_units_consumed == 0 {
return Err(InstructionError::BuiltinProgramsMustConsumeComputeUnits);
}
timings
.execute_accessories
.process_instructions
.process_executable_chain_us += process_executable_chain_time.end_as_us();
result
}
pub fn get_log_collector(&self) -> Option<Rc<RefCell<LogCollector>>> {
self.log_collector.clone()
}
pub fn consume_checked(&self, amount: u64) -> Result<(), Box<dyn std::error::Error>> {
let mut compute_meter = self.compute_meter.borrow_mut();
let exceeded = *compute_meter < amount;
*compute_meter = compute_meter.saturating_sub(amount);
if exceeded {
return Err(Box::new(InstructionError::ComputationalBudgetExceeded));
}
Ok(())
}
pub fn mock_set_remaining(&self, remaining: u64) {
*self.compute_meter.borrow_mut() = remaining;
}
pub fn get_compute_budget(&self) -> &ComputeBudget {
&self.compute_budget
}
pub fn get_feature_set(&self) -> &FeatureSet {
&self.environment_config.feature_set
}
pub fn mock_set_feature_set(&mut self, feature_set: Arc<FeatureSet>) {
self.environment_config.feature_set = feature_set;
}
pub fn get_sysvar_cache(&self) -> &SysvarCache {
self.environment_config.sysvar_cache
}
pub fn get_epoch_vote_account_stake(&self, pubkey: &'a Pubkey) -> u64 {
(self
.environment_config
.get_epoch_vote_account_stake_callback)(pubkey)
}
pub fn get_current_epoch(&self) -> u64 {
self.environment_config
.stakes_handle
.pending_epoch()
.saturating_sub(1)
}
pub fn get_next_epoch(&self) -> u64 {
self.environment_config.stakes_handle.pending_epoch()
}
pub fn get_last_freeze_timestamp(&self) -> u64 {
self.environment_config
.stakes_handle
.last_frozen_timestamp()
.unwrap_or(0)
}
pub fn mock_set_last_freeze_timestamp(&mut self, timestamp: u64) {
self.environment_config.stakes_handle.set_pending_epoch(1);
let history_entry = StakeCacheData {
epoch: 0,
timestamp,
..Default::default()
};
self.environment_config
.stakes_handle
.push_frozen(history_entry);
}
pub fn mock_insert_stake_account(
&mut self,
pubkey: rialo_s_pubkey::Pubkey,
account: rialo_stake_cache_interface::StakeAccount,
) {
self.environment_config
.stakes_handle
.insert_stake_account(pubkey, account);
}
pub fn freeze_stakes(&self) {
self.environment_config.stakes_handle.freeze_stakes();
}
pub fn signal_freeze_stakes(&self) {
self.environment_config
.stakes_handle
.set_epoch_stakes_frozen();
}
pub fn get_all_validator_accounts_from_last_frozen(&self) -> Vec<(Pubkey, ValidatorAccount)> {
self.environment_config
.stakes_handle
.get_all_validator_accounts_from_last_frozen()
}
pub fn frozen_stake_history_len(&self) -> usize {
self.environment_config.stakes_handle.frozen_len()
}
pub fn request_epoch_rewards_init(&self, epoch: u64, total_rewards: u64) {
self.environment_config
.stakes_handle
.request_epoch_rewards_init(epoch, total_rewards);
}
pub fn front_frozen_epoch(&self) -> Option<u64> {
self.environment_config.stakes_handle.front_frozen_epoch()
}
pub fn is_validator_referenced(
&self,
validator: &Pubkey,
validator_info: &rialo_stake_cache_interface::ValidatorInfo,
last_freeze_timestamp: u64,
current_timestamp: u64,
) -> bool {
self.environment_config
.stakes_handle
.is_validator_referenced(
validator,
validator_info,
last_freeze_timestamp,
current_timestamp,
)
}
pub fn has_locked_stakers(
&self,
validator: &Pubkey,
lockup_period: u64,
current_timestamp: u64,
) -> bool {
self.environment_config.stakes_handle.has_locked_stakers(
validator,
lockup_period,
current_timestamp,
)
}
pub fn is_validator_referenced_excluding_self_bond(
&self,
validator_pubkey: &Pubkey,
validator_info: &rialo_stake_cache_interface::ValidatorInfo,
last_freeze_timestamp: u64,
current_timestamp: u64,
) -> bool {
self.environment_config
.stakes_handle
.is_validator_referenced_excluding_self_bond(
validator_pubkey,
validator_info,
last_freeze_timestamp,
current_timestamp,
)
}
pub fn get_stake_account_from_pending(
&self,
pubkey: &Pubkey,
) -> Option<rialo_stake_cache_interface::StakeAccount> {
self.environment_config
.stakes_handle
.get_stake_account_from_pending(pubkey)
}
pub fn is_epoch_rewards_init_pending(&self) -> bool {
self.environment_config
.stakes_handle
.is_epoch_rewards_init_pending()
}
pub fn completed_frozen_epochs(&self) -> Vec<u64> {
self.environment_config
.stakes_handle
.completed_frozen_epochs()
}
pub fn epoch_rewards_exists(&self, epoch: u64) -> bool {
self.environment_config
.stakes_handle
.epoch_rewards_exists(epoch)
}
pub fn get_check_aligned(&self) -> bool {
self.transaction_context
.get_current_instruction_context()
.and_then(|instruction_context| {
let program_account =
instruction_context.try_borrow_last_program_account(self.transaction_context);
debug_assert!(program_account.is_ok());
program_account
})
.map(|program_account| *program_account.get_owner() != bpf_loader_deprecated::id())
.unwrap_or(true)
}
pub fn set_syscall_context(
&mut self,
syscall_context: SyscallContext,
) -> Result<(), InstructionError> {
*self
.syscall_context
.last_mut()
.ok_or(InstructionError::CallDepth)? = Some(syscall_context);
Ok(())
}
pub fn get_syscall_context(&self) -> Result<&SyscallContext, InstructionError> {
self.syscall_context
.last()
.and_then(std::option::Option::as_ref)
.ok_or(InstructionError::CallDepth)
}
pub fn get_syscall_context_mut(&mut self) -> Result<&mut SyscallContext, InstructionError> {
self.syscall_context
.last_mut()
.and_then(|syscall_context| syscall_context.as_mut())
.ok_or(InstructionError::CallDepth)
}
pub fn get_traces(&self) -> &Vec<Vec<[u64; 12]>> {
&self.traces
}
}
#[macro_export]
macro_rules! with_mock_invoke_context {
(
$invoke_context:ident,
$transaction_context:ident,
$entry:expr,
$transaction_accounts:expr $(,)?
) => {
use rialo_s_compute_budget::compute_budget::ComputeBudget;
use rialo_s_feature_set::FeatureSet;
use rialo_s_log_collector::LogCollector;
use rialo_s_type_overrides::sync::Arc;
use $crate::{
__private::{Hash, ReadableAccount, Rent, TransactionContext},
invoke_context::{EnvironmentConfig, InvokeContext},
loaded_programs::{ProgramCacheEntry, ProgramCacheForTx, ProgramCacheForTxBatch},
sysvar_cache::SysvarCache,
};
let compute_budget = ComputeBudget::default();
let mut $transaction_context = TransactionContext::new(
$transaction_accounts,
Rent::default(),
compute_budget.max_instruction_stack_depth,
compute_budget.max_instruction_trace_length,
);
let mut sysvar_cache = SysvarCache::default();
sysvar_cache.fill_missing_entries(|pubkey, callback| {
for index in 0..$transaction_context.get_number_of_accounts() {
if $transaction_context
.get_key_of_account_at_index(index)
.unwrap()
== pubkey
{
callback(
$transaction_context
.get_account_at_index(index)
.unwrap()
.borrow()
.data(),
);
}
}
});
let mock_stakes_handle = $crate::invoke_context::StakesHandle::default();
let environment_config = EnvironmentConfig::new(
Hash::default(),
0,
&|_| 0,
Arc::new(FeatureSet::all_enabled()),
&sysvar_cache,
0,
mock_stakes_handle,
);
let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default();
if let Some((loader_id, builtin)) = $entry {
program_cache_for_tx_batch.replenish(
loader_id,
Arc::new(ProgramCacheEntry::new_builtin(0, 0, builtin)),
);
}
let mut program_cache_for_tx = ProgramCacheForTx::from_cache(&program_cache_for_tx_batch);
let mut $invoke_context = InvokeContext::new(
&mut $transaction_context,
&mut program_cache_for_tx,
environment_config,
Some(LogCollector::new_ref()),
compute_budget,
);
};
(
$invoke_context:ident,
$transaction_context:ident,
$transaction_accounts:expr $(,)?
) => {
with_mock_invoke_context!(
$invoke_context,
$transaction_context,
None,
$transaction_accounts
)
};
}
pub fn mock_process_instruction<
T: Into<StoredAccount>,
F: FnMut(&mut InvokeContext<'_, '_>),
G: FnMut(&mut InvokeContext<'_, '_>),
>(
loader_id: &Pubkey,
mut program_indices: Vec<IndexOfAccount>,
instruction_data: &[u8],
transaction_accounts: Vec<(Pubkey, T)>,
instruction_account_metas: Vec<AccountMeta>,
expected_result: Result<(), InstructionError>,
builtin_function: BuiltinFunctionWithContext,
mut pre_adjustments: F,
mut post_adjustments: G,
) -> Vec<StoredAccount> {
let mut transaction_accounts: Vec<TransactionAccount> = transaction_accounts
.into_iter()
.map(|(key, account)| (key, account.into()))
.collect();
let mut instruction_accounts: Vec<InstructionAccount> =
Vec::with_capacity(instruction_account_metas.len());
for (instruction_account_index, account_meta) in instruction_account_metas.iter().enumerate() {
let index_in_transaction = transaction_accounts
.iter()
.position(|(key, _account)| *key == account_meta.pubkey)
.unwrap_or(transaction_accounts.len())
as IndexOfAccount;
let index_in_callee = instruction_accounts
.get(0..instruction_account_index)
.unwrap()
.iter()
.position(|instruction_account| {
instruction_account.index_in_transaction == index_in_transaction
})
.unwrap_or(instruction_account_index) as IndexOfAccount;
instruction_accounts.push(InstructionAccount {
index_in_transaction,
index_in_caller: index_in_transaction,
index_in_callee,
is_signer: account_meta.is_signer,
is_writable: account_meta.is_writable,
});
}
if program_indices.is_empty() {
program_indices.insert(0, transaction_accounts.len() as IndexOfAccount);
let processor_account =
StoredAccount::Data(AccountSharedData::new(0, 0, &native_loader::id()));
transaction_accounts.push((*loader_id, processor_account));
}
let pop_epoch_schedule_account = if !transaction_accounts
.iter()
.any(|(key, _)| *key == sysvar::epoch_schedule::id())
{
transaction_accounts.push((
sysvar::epoch_schedule::id(),
create_account_shared_data_for_test(&EpochSchedule::default()),
));
true
} else {
false
};
with_mock_invoke_context!(
invoke_context,
transaction_context,
Some((*loader_id, builtin_function)),
transaction_accounts
);
pre_adjustments(&mut invoke_context);
let result = invoke_context.process_instruction(
instruction_data,
&instruction_accounts,
&program_indices,
&mut 0,
&mut ExecuteTimings::default(),
);
assert_eq!(result, expected_result);
post_adjustments(&mut invoke_context);
drop(invoke_context);
let mut transaction_accounts = transaction_context.deconstruct_without_keys().unwrap();
if pop_epoch_schedule_account {
transaction_accounts.pop();
}
transaction_accounts.pop();
transaction_accounts
}
#[cfg(test)]
mod tests {
use rialo_s_compute_budget::compute_budget_limits;
use rialo_s_instruction::Instruction;
use rialo_s_rent::Rent;
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Debug, Serialize, Deserialize)]
enum MockInstruction {
NoopSuccess,
NoopFail,
ModifyOwned,
ModifyNotOwned,
ModifyReadonly,
UnbalancedPush,
UnbalancedPop,
ConsumeComputeUnits {
compute_units_to_consume: u64,
desired_result: Result<(), InstructionError>,
},
Resize {
new_len: u64,
},
}
const MOCK_BUILTIN_COMPUTE_UNIT_COST: u64 = 1;
declare_process_instruction!(
MockBuiltin,
MOCK_BUILTIN_COMPUTE_UNIT_COST,
|invoke_context| {
let transaction_context = &invoke_context.transaction_context;
let instruction_context = transaction_context.get_current_instruction_context()?;
let instruction_data = instruction_context.get_instruction_data();
let program_id = instruction_context.get_last_program_key(transaction_context)?;
let instruction_accounts = (0..4)
.map(|instruction_account_index| InstructionAccount {
index_in_transaction: instruction_account_index,
index_in_caller: instruction_account_index,
index_in_callee: instruction_account_index,
is_signer: false,
is_writable: false,
})
.collect::<Vec<_>>();
assert_eq!(
program_id,
instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.get_owner()
);
assert_ne!(
instruction_context
.try_borrow_instruction_account(transaction_context, 1)?
.get_owner(),
instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.get_key()
);
if let Ok(instruction) = bincode::deserialize(instruction_data) {
match instruction {
MockInstruction::NoopSuccess => (),
MockInstruction::NoopFail => return Err(InstructionError::GenericError),
MockInstruction::ModifyOwned => instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.set_data_from_slice(&[1])?,
MockInstruction::ModifyNotOwned => instruction_context
.try_borrow_instruction_account(transaction_context, 1)?
.set_data_from_slice(&[1])?,
MockInstruction::ModifyReadonly => instruction_context
.try_borrow_instruction_account(transaction_context, 2)?
.set_data_from_slice(&[1])?,
MockInstruction::UnbalancedPush => {
instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.checked_add_kelvins(1)?;
let program_id = *transaction_context.get_key_of_account_at_index(3)?;
let metas = vec![
AccountMeta::new_readonly(
*transaction_context.get_key_of_account_at_index(0)?,
false,
),
AccountMeta::new_readonly(
*transaction_context.get_key_of_account_at_index(1)?,
false,
),
];
let inner_instruction = Instruction::new_with_bincode(
program_id,
&MockInstruction::NoopSuccess,
metas,
);
invoke_context
.transaction_context
.get_next_instruction_context()
.unwrap()
.configure(&[3], &instruction_accounts, &[]);
let result = invoke_context.push();
assert_eq!(result, Err(InstructionError::UnbalancedInstruction));
result?;
invoke_context
.native_invoke(inner_instruction.into(), &[])
.and(invoke_context.pop())?;
}
MockInstruction::UnbalancedPop => instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.checked_add_kelvins(1)?,
MockInstruction::ConsumeComputeUnits {
compute_units_to_consume,
desired_result,
} => {
invoke_context
.consume_checked(compute_units_to_consume)
.map_err(|_| InstructionError::ComputationalBudgetExceeded)?;
return desired_result;
}
MockInstruction::Resize { new_len } => instruction_context
.try_borrow_instruction_account(transaction_context, 0)?
.set_data(vec![0; new_len as usize])?,
}
} else {
return Err(InstructionError::InvalidInstructionData);
}
Ok(())
}
);
#[test]
fn test_instruction_stack_height() {
let one_more_than_max_depth = ComputeBudget::default()
.max_instruction_stack_depth
.saturating_add(1);
let mut invoke_stack = vec![];
let mut transaction_accounts = vec![];
let mut instruction_accounts = vec![];
for index in 0..one_more_than_max_depth {
invoke_stack.push(rialo_s_pubkey::new_rand());
transaction_accounts.push((
rialo_s_pubkey::new_rand(),
AccountSharedData::new(index as u64, 1, invoke_stack.get(index).unwrap()),
));
instruction_accounts.push(InstructionAccount {
index_in_transaction: index as IndexOfAccount,
index_in_caller: index as IndexOfAccount,
index_in_callee: instruction_accounts.len() as IndexOfAccount,
is_signer: false,
is_writable: true,
});
}
for (index, program_id) in invoke_stack.iter().enumerate() {
transaction_accounts.push((
*program_id,
AccountSharedData::new(1, 1, &rialo_s_pubkey::Pubkey::default()),
));
instruction_accounts.push(InstructionAccount {
index_in_transaction: index as IndexOfAccount,
index_in_caller: index as IndexOfAccount,
index_in_callee: index as IndexOfAccount,
is_signer: false,
is_writable: false,
});
}
let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
let mut depth_reached = 0;
for _ in 0..invoke_stack.len() {
invoke_context
.transaction_context
.get_next_instruction_context()
.unwrap()
.configure(
&[one_more_than_max_depth.saturating_add(depth_reached) as IndexOfAccount],
&instruction_accounts,
&[],
);
if Err(InstructionError::CallDepth) == invoke_context.push() {
break;
}
depth_reached = depth_reached.saturating_add(1);
}
assert_ne!(depth_reached, 0);
assert!(depth_reached < one_more_than_max_depth);
}
#[test]
fn test_max_instruction_trace_length() {
const MAX_INSTRUCTIONS: usize = 8;
let mut transaction_context = TransactionContext::new::<StoredAccount>(
Vec::new(),
Rent::default(),
1,
MAX_INSTRUCTIONS,
);
for _ in 0..MAX_INSTRUCTIONS {
transaction_context.push().unwrap();
transaction_context.pop().unwrap();
}
assert_eq!(
transaction_context.push(),
Err(InstructionError::MaxInstructionTraceLengthExceeded)
);
}
#[test]
fn test_process_instruction() {
let callee_program_id = rialo_s_pubkey::new_rand();
let owned_account = AccountSharedData::new(42, 1, &callee_program_id);
let not_owned_account = AccountSharedData::new(84, 1, &rialo_s_pubkey::new_rand());
let readonly_account = AccountSharedData::new(168, 1, &rialo_s_pubkey::new_rand());
let loader_account = AccountSharedData::new(0, 1, &native_loader::id());
let program_account = AccountSharedData::new(1, 1, &native_loader::id());
let transaction_accounts = vec![
(rialo_s_pubkey::new_rand(), owned_account),
(rialo_s_pubkey::new_rand(), not_owned_account),
(rialo_s_pubkey::new_rand(), readonly_account),
(callee_program_id, program_account),
(rialo_s_pubkey::new_rand(), loader_account),
];
let metas = vec![
AccountMeta::new(transaction_accounts.first().unwrap().0, false),
AccountMeta::new(transaction_accounts.get(1).unwrap().0, false),
AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false),
];
let instruction_accounts = (0..4)
.map(|instruction_account_index| InstructionAccount {
index_in_transaction: instruction_account_index,
index_in_caller: instruction_account_index,
index_in_callee: instruction_account_index,
is_signer: false,
is_writable: instruction_account_index < 2,
})
.collect::<Vec<_>>();
let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
with_mock_invoke_context!(
invoke_context,
transaction_context,
Some((callee_program_id, MockBuiltin::vm)),
transaction_accounts
);
let cases = vec![
(MockInstruction::NoopSuccess, Ok(())),
(
MockInstruction::NoopFail,
Err(InstructionError::GenericError),
),
(MockInstruction::ModifyOwned, Ok(())),
(
MockInstruction::ModifyNotOwned,
Err(InstructionError::ExternalAccountDataModified),
),
(
MockInstruction::ModifyReadonly,
Err(InstructionError::ReadonlyDataModified),
),
(
MockInstruction::UnbalancedPush,
Err(InstructionError::UnbalancedInstruction),
),
(
MockInstruction::UnbalancedPop,
Err(InstructionError::UnbalancedInstruction),
),
];
for case in cases {
invoke_context
.transaction_context
.get_next_instruction_context()
.unwrap()
.configure(&[4], &instruction_accounts, &[]);
invoke_context.push().unwrap();
let inner_instruction =
Instruction::new_with_bincode(callee_program_id, &case.0, metas.clone());
let result = invoke_context
.native_invoke(inner_instruction.into(), &[])
.and(invoke_context.pop());
assert_eq!(result, case.1);
}
let compute_units_to_consume = 10;
let expected_results = vec![Ok(()), Err(InstructionError::GenericError)];
for expected_result in expected_results {
invoke_context
.transaction_context
.get_next_instruction_context()
.unwrap()
.configure(&[4], &instruction_accounts, &[]);
invoke_context.push().unwrap();
let inner_instruction = Instruction::new_with_bincode(
callee_program_id,
&MockInstruction::ConsumeComputeUnits {
compute_units_to_consume,
desired_result: expected_result.clone(),
},
metas.clone(),
);
let inner_instruction = StableInstruction::from(inner_instruction);
let (inner_instruction_accounts, program_indices) = invoke_context
.prepare_instruction(&inner_instruction, &[])
.unwrap();
let mut compute_units_consumed = 0;
let result = invoke_context.process_instruction(
&inner_instruction.data,
&inner_instruction_accounts,
&program_indices,
&mut compute_units_consumed,
&mut ExecuteTimings::default(),
);
assert!(compute_units_consumed > 0);
assert_eq!(
compute_units_consumed,
compute_units_to_consume.saturating_add(MOCK_BUILTIN_COMPUTE_UNIT_COST),
);
assert_eq!(result, expected_result);
invoke_context.pop().unwrap();
}
}
#[test]
fn test_invoke_context_compute_budget() {
let transaction_accounts = vec![(rialo_s_pubkey::new_rand(), AccountSharedData::default())];
let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
invoke_context.compute_budget = ComputeBudget::new(
compute_budget_limits::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
);
invoke_context
.transaction_context
.get_next_instruction_context()
.unwrap()
.configure(&[0], &[], &[]);
invoke_context.push().unwrap();
assert_eq!(
*invoke_context.get_compute_budget(),
ComputeBudget::new(
compute_budget_limits::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64
)
);
invoke_context.pop().unwrap();
}
#[test]
fn test_process_instruction_accounts_resize_delta() {
let program_key = Pubkey::new_unique();
let user_account_data_len = 123u64;
let user_account =
AccountSharedData::new(100, user_account_data_len as usize, &program_key);
let dummy_account = AccountSharedData::new(10, 0, &program_key);
let program_account = AccountSharedData::new(500, 500, &native_loader::id());
let transaction_accounts = vec![
(Pubkey::new_unique(), user_account),
(Pubkey::new_unique(), dummy_account),
(program_key, program_account),
];
let instruction_accounts = [
InstructionAccount {
index_in_transaction: 0,
index_in_caller: 0,
index_in_callee: 0,
is_signer: false,
is_writable: true,
},
InstructionAccount {
index_in_transaction: 1,
index_in_caller: 1,
index_in_callee: 1,
is_signer: false,
is_writable: false,
},
];
let transaction_accounts: Vec<(Pubkey, StoredAccount)> = transaction_accounts
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
with_mock_invoke_context!(
invoke_context,
transaction_context,
Some((program_key, MockBuiltin::vm)),
transaction_accounts
);
{
let resize_delta: i64 = 0;
let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
let instruction_data =
bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
let result = invoke_context.process_instruction(
&instruction_data,
&instruction_accounts,
&[2],
&mut 0,
&mut ExecuteTimings::default(),
);
assert!(result.is_ok());
assert_eq!(
invoke_context
.transaction_context
.accounts_resize_delta()
.unwrap(),
resize_delta
);
}
{
let resize_delta: i64 = 1;
let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
let instruction_data =
bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
let result = invoke_context.process_instruction(
&instruction_data,
&instruction_accounts,
&[2],
&mut 0,
&mut ExecuteTimings::default(),
);
assert!(result.is_ok());
assert_eq!(
invoke_context
.transaction_context
.accounts_resize_delta()
.unwrap(),
resize_delta
);
}
{
let resize_delta: i64 = -1;
let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64;
let instruction_data =
bincode::serialize(&MockInstruction::Resize { new_len }).unwrap();
let result = invoke_context.process_instruction(
&instruction_data,
&instruction_accounts,
&[2],
&mut 0,
&mut ExecuteTimings::default(),
);
assert!(result.is_ok());
assert_eq!(
invoke_context
.transaction_context
.accounts_resize_delta()
.unwrap(),
resize_delta
);
}
}
}