#[cfg(feature = "register-tracing")]
use crate::register_tracing::DefaultRegisterTracingCallback;
#[cfg(feature = "precompiles")]
use precompiles::load_precompiles;
#[cfg(feature = "nodejs-internal")]
use qualifier_attr::qualifiers;
#[allow(deprecated)]
use solana_sysvar::recent_blockhashes::IterItem;
#[allow(deprecated)]
use solana_sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes};
use {
crate::{
accounts_db::AccountsDb,
error::LiteSVMError,
history::TransactionHistory,
message_processor::process_message,
programs::load_default_programs,
types::{
ExecutionResult, FailedTransactionMetadata, TransactionMetadata, TransactionResult,
},
utils::{
create_blockhash,
rent::{check_rent_state_with_account, get_account_rent_state, RentState},
},
},
agave_feature_set::{
increase_cpi_account_info_limit, raise_cpi_nesting_limit_to_8, FeatureSet,
},
agave_reserved_account_keys::ReservedAccountKeys,
agave_syscalls::{
create_program_runtime_environment_v1, create_program_runtime_environment_v2,
},
log::error,
serde::de::DeserializeOwned,
solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount},
solana_builtins::BUILTINS,
solana_clock::Clock,
solana_compute_budget::{
compute_budget::{ComputeBudget, SVMTransactionExecutionCost},
compute_budget_limits::ComputeBudgetLimits,
},
solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions,
solana_epoch_rewards::EpochRewards,
solana_epoch_schedule::EpochSchedule,
solana_fee::FeeFeatures,
solana_fee_structure::FeeStructure,
solana_hash::Hash,
solana_keypair::Keypair,
solana_last_restart_slot::LastRestartSlot,
solana_message::{
inner_instruction::InnerInstructionsList, Message, SanitizedMessage, VersionedMessage,
},
solana_native_token::LAMPORTS_PER_SOL,
solana_nonce::{state::DurableNonce, NONCED_TX_MARKER_IX_INDEX},
solana_program_runtime::{
invoke_context::{BuiltinFunctionWithContext, EnvironmentConfig, InvokeContext},
loaded_programs::{LoadProgramMetrics, ProgramCacheEntry},
},
solana_pubkey::Pubkey,
solana_rent::Rent,
solana_sdk_ids::{bpf_loader, native_loader, system_program},
solana_signature::Signature,
solana_signer::Signer,
solana_slot_hashes::SlotHashes,
solana_slot_history::SlotHistory,
solana_stake_interface::stake_history::StakeHistory,
solana_svm_log_collector::LogCollector,
solana_svm_timings::ExecuteTimings,
solana_svm_transaction::svm_message::SVMMessage,
solana_system_program::{get_system_account_kind, SystemAccountKind},
solana_sysvar::{Sysvar, SysvarSerialize},
solana_sysvar_id::SysvarId,
solana_transaction::{
sanitized::{MessageHash, SanitizedTransaction},
versioned::VersionedTransaction,
},
solana_transaction_context::{ExecutionRecord, IndexOfAccount, TransactionContext},
solana_transaction_error::TransactionError,
std::{cell::RefCell, path::Path, rc::Rc, sync::Arc},
types::SimulatedTransactionInfo,
utils::{
construct_instructions_account,
inner_instructions::inner_instructions_list_from_instruction_trace,
},
};
pub mod error;
pub mod types;
mod accounts_db;
mod callback;
mod format_logs;
mod history;
mod message_processor;
#[cfg(feature = "precompiles")]
mod precompiles;
mod programs;
#[cfg(feature = "register-tracing")]
mod register_tracing;
mod utils;
#[derive(Clone)]
pub struct LiteSVM {
accounts: AccountsDb,
airdrop_kp: [u8; 64],
feature_set: FeatureSet,
reserved_account_keys: ReservedAccountKeys,
latest_blockhash: Hash,
history: TransactionHistory,
compute_budget: Option<ComputeBudget>,
sigverify: bool,
blockhash_check: bool,
fee_structure: FeeStructure,
log_bytes_limit: Option<usize>,
#[cfg(feature = "invocation-inspect-callback")]
invocation_inspect_callback: Arc<dyn InvocationInspectCallback>,
#[cfg(feature = "invocation-inspect-callback")]
enable_register_tracing: bool,
}
impl Default for LiteSVM {
fn default() -> Self {
let _enable_register_tracing = false;
#[cfg(feature = "register-tracing")]
let _enable_register_tracing = std::env::var("SBF_TRACE_DIR").is_ok();
Self::new_inner(_enable_register_tracing)
}
}
impl LiteSVM {
fn new_inner(_enable_register_tracing: bool) -> Self {
let feature_set = FeatureSet::default();
#[allow(unused_mut)]
let mut svm = Self {
accounts: Default::default(),
airdrop_kp: Keypair::new().to_bytes(),
reserved_account_keys: Self::reserved_account_keys_for_feature_set(&feature_set),
feature_set,
latest_blockhash: create_blockhash(b"genesis"),
history: TransactionHistory::new(),
compute_budget: None,
sigverify: false,
blockhash_check: false,
fee_structure: FeeStructure::default(),
log_bytes_limit: Some(10_000),
#[cfg(feature = "invocation-inspect-callback")]
enable_register_tracing: _enable_register_tracing,
#[cfg(feature = "invocation-inspect-callback")]
invocation_inspect_callback: Arc::new(EmptyInvocationInspectCallback {}),
};
#[cfg(feature = "register-tracing")]
if svm.enable_register_tracing {
svm.invocation_inspect_callback = Arc::new(DefaultRegisterTracingCallback::default());
}
svm
}
fn into_basic(self) -> Self {
let svm = self
.with_feature_set(FeatureSet::all_enabled())
.with_builtins()
.with_lamports(1_000_000u64.wrapping_mul(LAMPORTS_PER_SOL))
.with_sysvars()
.with_default_programs()
.with_sigverify(true)
.with_blockhash_check(true);
#[cfg(feature = "precompiles")]
let svm = svm.with_precompiles();
svm
}
pub fn new() -> Self {
LiteSVM::default().into_basic()
}
#[cfg(feature = "register-tracing")]
pub fn new_debuggable(enable_register_tracing: bool) -> Self {
Self::new_inner(enable_register_tracing).into_basic()
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_compute_budget(&mut self, compute_budget: ComputeBudget) {
self.compute_budget = Some(compute_budget);
}
pub fn with_compute_budget(mut self, compute_budget: ComputeBudget) -> Self {
self.set_compute_budget(compute_budget);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_sigverify(&mut self, sigverify: bool) {
self.sigverify = sigverify;
}
pub fn with_sigverify(mut self, sigverify: bool) -> Self {
self.set_sigverify(sigverify);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_blockhash_check(&mut self, check: bool) {
self.blockhash_check = check;
}
pub fn with_blockhash_check(mut self, check: bool) -> Self {
self.set_blockhash_check(check);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_sysvars(&mut self) {
self.set_sysvar(&Clock::default());
self.set_sysvar(&EpochRewards::default());
self.set_sysvar(&EpochSchedule::default());
#[allow(deprecated)]
let fees = Fees::default();
self.set_sysvar(&fees);
self.set_sysvar(&LastRestartSlot::default());
let latest_blockhash = self.latest_blockhash;
#[allow(deprecated)]
self.set_sysvar(&RecentBlockhashes::from_iter([IterItem(
0,
&latest_blockhash,
fees.fee_calculator.lamports_per_signature,
)]));
self.set_sysvar(&Rent::default());
self.set_sysvar(&SlotHashes::new(&[(
self.accounts.sysvar_cache.get_clock().unwrap().slot,
latest_blockhash,
)]));
self.set_sysvar(&SlotHistory::default());
self.set_sysvar(&StakeHistory::default());
}
pub fn with_sysvars(mut self) -> Self {
self.set_sysvars();
self
}
pub fn with_feature_set(mut self, feature_set: FeatureSet) -> Self {
self.set_feature_set(feature_set);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_feature_set(&mut self, feature_set: FeatureSet) {
self.feature_set = feature_set;
self.reserved_account_keys = Self::reserved_account_keys_for_feature_set(&self.feature_set);
}
fn reserved_account_keys_for_feature_set(feature_set: &FeatureSet) -> ReservedAccountKeys {
let mut reserved_account_keys = ReservedAccountKeys::default();
reserved_account_keys.update_active_set(feature_set);
reserved_account_keys
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_builtins(&mut self) {
BUILTINS.iter().for_each(|builtint| {
if builtint
.enable_feature_id
.is_none_or(|x| self.feature_set.is_active(&x))
{
let loaded_program =
ProgramCacheEntry::new_builtin(0, builtint.name.len(), builtint.entrypoint);
self.accounts
.programs_cache
.replenish(builtint.program_id, Arc::new(loaded_program));
self.accounts.add_builtin_account(
builtint.program_id,
crate::utils::create_loadable_account_for_test(builtint.name),
);
}
});
let _enable_register_tracing = false;
#[cfg(feature = "register-tracing")]
let _enable_register_tracing = self.enable_register_tracing;
let compute_budget = self
.compute_budget
.unwrap_or(ComputeBudget::new_with_defaults(
self.feature_set
.is_active(&raise_cpi_nesting_limit_to_8::ID),
self.feature_set
.is_active(&increase_cpi_account_info_limit::ID),
));
let program_runtime_v1 = create_program_runtime_environment_v1(
&self.feature_set.runtime_features(),
&compute_budget.to_budget(),
false,
_enable_register_tracing,
)
.unwrap();
let program_runtime_v2 =
create_program_runtime_environment_v2(&compute_budget.to_budget(), true);
self.accounts.environments.program_runtime_v1 = Arc::new(program_runtime_v1);
self.accounts.environments.program_runtime_v2 = Arc::new(program_runtime_v2);
}
pub fn with_builtins(mut self) -> Self {
self.set_builtins();
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_lamports(&mut self, lamports: u64) {
self.accounts.add_account_no_checks(
Keypair::try_from(self.airdrop_kp.as_slice())
.unwrap()
.pubkey(),
AccountSharedData::new(lamports, 0, &system_program::id()),
);
}
pub fn with_lamports(mut self, lamports: u64) -> Self {
self.set_lamports(lamports);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_default_programs(&mut self) {
load_default_programs(self);
}
pub fn with_default_programs(mut self) -> Self {
self.set_default_programs();
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_transaction_history(&mut self, capacity: usize) {
self.history.set_capacity(capacity);
}
pub fn with_transaction_history(mut self, capacity: usize) -> Self {
self.set_transaction_history(capacity);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
fn set_log_bytes_limit(&mut self, limit: Option<usize>) {
self.log_bytes_limit = limit;
}
pub fn with_log_bytes_limit(mut self, limit: Option<usize>) -> Self {
self.set_log_bytes_limit(limit);
self
}
#[cfg_attr(feature = "nodejs-internal", qualifiers(pub))]
#[cfg(feature = "precompiles")]
fn set_precompiles(&mut self) {
load_precompiles(self);
}
#[cfg(feature = "precompiles")]
pub fn with_precompiles(mut self) -> Self {
self.set_precompiles();
self
}
pub fn minimum_balance_for_rent_exemption(&self, data_len: usize) -> u64 {
1.max(
self.accounts
.sysvar_cache
.get_rent()
.unwrap_or_default()
.minimum_balance(data_len),
)
}
pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
self.accounts.get_account(pubkey).map(Into::into)
}
pub fn set_account(&mut self, pubkey: Pubkey, data: Account) -> Result<(), LiteSVMError> {
self.accounts.add_account(pubkey, data.into())
}
pub fn accounts_db(&self) -> &AccountsDb {
&self.accounts
}
pub fn get_balance(&self, pubkey: &Pubkey) -> Option<u64> {
self.accounts.get_account_ref(pubkey).map(|x| x.lamports())
}
pub fn latest_blockhash(&self) -> Hash {
self.latest_blockhash
}
pub fn set_sysvar<T>(&mut self, sysvar: &T)
where
T: Sysvar + SysvarId + SysvarSerialize,
{
let mut account = AccountSharedData::new(1, T::size_of(), &solana_sdk_ids::sysvar::id());
account.serialize_data(sysvar).unwrap();
self.accounts.add_account(T::id(), account).unwrap();
}
pub fn get_sysvar<T>(&self) -> T
where
T: Sysvar + SysvarId + DeserializeOwned,
{
bincode::deserialize(self.accounts.get_account_ref(&T::id()).unwrap().data()).unwrap()
}
pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> {
self.history.get_transaction(signature)
}
pub fn airdrop_pubkey(&self) -> Pubkey {
Keypair::try_from(self.airdrop_kp.as_slice())
.unwrap()
.pubkey()
}
pub fn airdrop(&mut self, pubkey: &Pubkey, lamports: u64) -> TransactionResult {
let payer = Keypair::try_from(self.airdrop_kp.as_slice()).unwrap();
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(Message::new_with_blockhash(
&[solana_system_interface::instruction::transfer(
&payer.pubkey(),
pubkey,
lamports,
)],
Some(&payer.pubkey()),
&self.latest_blockhash,
)),
&[payer],
)
.unwrap();
self.send_transaction(tx)
}
pub fn add_builtin(&mut self, program_id: Pubkey, entrypoint: BuiltinFunctionWithContext) {
let builtin = ProgramCacheEntry::new_builtin(
self.accounts
.sysvar_cache
.get_clock()
.unwrap_or_default()
.slot,
1,
entrypoint,
);
self.accounts
.programs_cache
.replenish(program_id, Arc::new(builtin));
let mut account = AccountSharedData::new(1, 1, &bpf_loader::id());
account.set_executable(true);
self.accounts.add_account_no_checks(program_id, account);
}
pub fn add_program_from_file(
&mut self,
program_id: impl Into<Pubkey>,
path: impl AsRef<Path>,
) -> Result<(), LiteSVMError> {
let bytes = std::fs::read(path)?;
self.add_program(program_id, &bytes)?;
Ok(())
}
pub fn add_program(
&mut self,
program_id: impl Into<Pubkey>,
program_bytes: &[u8],
) -> Result<(), LiteSVMError> {
let program_id = program_id.into();
let program_len = program_bytes.len();
let lamports = self.minimum_balance_for_rent_exemption(program_len);
let mut account = AccountSharedData::new(lamports, program_len, &bpf_loader::id());
account.set_executable(true);
account.set_data_from_slice(program_bytes);
let current_slot = self
.accounts
.sysvar_cache
.get_clock()
.unwrap_or_default()
.slot;
let mut loaded_program = solana_bpf_loader_program::load_program_from_bytes(
None,
&mut LoadProgramMetrics::default(),
account.data(),
account.owner(),
account.data().len(),
current_slot,
self.accounts.environments.program_runtime_v1.clone(),
false,
)
.unwrap_or_default();
loaded_program.effective_slot = current_slot;
self.accounts.add_account(program_id, account)?;
self.accounts
.programs_cache
.replenish(program_id, Arc::new(loaded_program));
Ok(())
}
fn create_transaction_context(
&self,
compute_budget: ComputeBudget,
accounts: Vec<(Pubkey, AccountSharedData)>,
) -> TransactionContext<'_> {
TransactionContext::new(
accounts,
self.get_sysvar(),
compute_budget.max_instruction_stack_depth,
compute_budget.max_instruction_trace_length,
)
}
fn sanitize_transaction_no_verify_inner(
&self,
tx: VersionedTransaction,
) -> Result<SanitizedTransaction, TransactionError> {
let res = SanitizedTransaction::try_create(
tx,
MessageHash::Compute,
Some(false),
&self.accounts,
&self.reserved_account_keys.active,
);
res.inspect_err(|_| {
log::error!("Transaction sanitization failed");
})
}
fn sanitize_transaction_no_verify(
&self,
tx: VersionedTransaction,
) -> Result<SanitizedTransaction, ExecutionResult> {
self.sanitize_transaction_no_verify_inner(tx)
.map_err(|err| ExecutionResult {
tx_result: Err(err),
..Default::default()
})
}
fn sanitize_transaction(
&self,
tx: VersionedTransaction,
) -> Result<SanitizedTransaction, ExecutionResult> {
self.sanitize_transaction_inner(tx)
.map_err(|err| ExecutionResult {
tx_result: Err(err),
..Default::default()
})
}
fn sanitize_transaction_inner(
&self,
tx: VersionedTransaction,
) -> Result<SanitizedTransaction, TransactionError> {
let tx = self.sanitize_transaction_no_verify_inner(tx)?;
tx.verify()?;
Ok(tx)
}
fn process_transaction<'a, 'b>(
&'a self,
tx: &'b SanitizedTransaction,
compute_budget_limits: ComputeBudgetLimits,
log_collector: Rc<RefCell<LogCollector>>,
) -> (
Result<(), TransactionError>,
u64,
Option<TransactionContext<'b>>,
u64,
Option<Pubkey>,
)
where
'a: 'b,
{
let compute_budget = self.compute_budget.unwrap_or_else(|| ComputeBudget {
compute_unit_limit: u64::from(compute_budget_limits.compute_unit_limit),
heap_size: compute_budget_limits.updated_heap_bytes,
..ComputeBudget::new_with_defaults(
self.feature_set
.is_active(&raise_cpi_nesting_limit_to_8::ID),
self.feature_set
.is_active(&increase_cpi_account_info_limit::ID),
)
});
let rent = self.accounts.sysvar_cache.get_rent().unwrap();
let message = tx.message();
let blockhash = message.recent_blockhash();
let mut program_cache_for_tx_batch = self.accounts.programs_cache.clone();
let mut accumulated_consume_units = 0;
let account_keys = message.account_keys();
let prioritization_fee = compute_budget_limits.get_prioritization_fee();
let fee = solana_fee::calculate_fee(
message,
false,
self.fee_structure.lamports_per_signature,
prioritization_fee,
FeeFeatures::from(&self.feature_set),
);
let mut validated_fee_payer = false;
let mut payer_key = None;
let maybe_accounts = account_keys
.iter()
.enumerate()
.map(|(i, key)| {
let account = if solana_sdk_ids::sysvar::instructions::check_id(key) {
construct_instructions_account(message)
} else {
let is_instruction_account = message.is_instruction_account(i);
let mut account = if !is_instruction_account
&& !message.is_writable(i)
&& self.accounts.programs_cache.find(key).is_some()
{
self.accounts.get_account(key).unwrap()
} else {
self.accounts.get_account(key).unwrap_or_else(|| {
let mut default_account = AccountSharedData::default();
default_account.set_rent_epoch(0);
default_account
})
};
if !validated_fee_payer && (!message.is_invoked(i) || is_instruction_account) {
validate_fee_payer(key, &mut account, i as IndexOfAccount, &rent, fee)?;
validated_fee_payer = true;
payer_key = Some(*key);
}
account
};
Ok((*key, account))
})
.collect::<solana_transaction_error::TransactionResult<Vec<_>>>();
let mut accounts = match maybe_accounts {
Ok(accs) => accs,
Err(e) => {
return (Err(e), accumulated_consume_units, None, fee, payer_key);
}
};
if !validated_fee_payer {
error!("Failed to validate fee payer");
return (
Err(TransactionError::AccountNotFound),
accumulated_consume_units,
None,
fee,
payer_key,
);
}
let builtins_start_index = accounts.len();
let maybe_program_indices = tx
.message()
.instructions()
.iter()
.map(|c| {
let program_index = c.program_id_index as usize;
let (program_id, program_account) = accounts.get(program_index).unwrap();
if native_loader::check_id(program_id) {
return Ok(program_index as IndexOfAccount);
}
if !program_account.executable() {
error!("Program account {program_id} is not executable.");
return Err(TransactionError::InvalidProgramForExecution);
}
let owner_id = program_account.owner();
if native_loader::check_id(owner_id) {
return Ok(program_index as IndexOfAccount);
}
if !accounts
.get(builtins_start_index..)
.ok_or(TransactionError::ProgramAccountNotFound)?
.iter()
.any(|(key, _)| key == owner_id)
{
let owner_account = self.accounts.get_account(owner_id).unwrap();
if !native_loader::check_id(owner_account.owner()) {
error!(
"Owner account {owner_id} is not owned by the native loader program."
);
return Err(TransactionError::InvalidProgramForExecution);
}
if !owner_account.executable() {
error!("Owner account {owner_id} is not executable");
return Err(TransactionError::InvalidProgramForExecution);
}
accounts.push((*owner_id, owner_account));
}
Ok(program_index as IndexOfAccount)
})
.collect::<Result<Vec<u16>, TransactionError>>();
match maybe_program_indices {
Ok(program_indices) => {
let mut context = self.create_transaction_context(compute_budget, accounts);
let feature_set = self.feature_set.runtime_features();
let mut invoke_context = InvokeContext::new(
&mut context,
&mut program_cache_for_tx_batch,
EnvironmentConfig::new(
*blockhash,
self.fee_structure.lamports_per_signature,
self,
&feature_set,
&self.accounts.environments,
&self.accounts.environments,
&self.accounts.sysvar_cache,
),
Some(log_collector),
compute_budget.to_budget(),
SVMTransactionExecutionCost::default(),
);
#[cfg(feature = "invocation-inspect-callback")]
self.invocation_inspect_callback.before_invocation(
tx,
&program_indices,
&invoke_context,
);
let mut tx_result = process_message(
message,
&program_indices,
&mut invoke_context,
&mut ExecuteTimings::default(),
&mut accumulated_consume_units,
)
.map(|_| ());
#[cfg(feature = "invocation-inspect-callback")]
self.invocation_inspect_callback
.after_invocation(&invoke_context, self.enable_register_tracing);
if let Err(err) = self.check_accounts_rent(tx, &context, &rent) {
tx_result = Err(err);
};
(
tx_result,
accumulated_consume_units,
Some(context),
fee,
payer_key,
)
}
Err(e) => (Err(e), accumulated_consume_units, None, fee, payer_key),
}
}
fn check_accounts_rent(
&self,
tx: &SanitizedTransaction,
context: &TransactionContext,
rent: &Rent,
) -> Result<(), TransactionError> {
let message = tx.message();
for index in 0..message.account_keys().len() {
if message.is_writable(index) {
let account = context
.accounts()
.try_borrow(index as IndexOfAccount)
.map_err(|err| TransactionError::InstructionError(index as u8, err))?;
let pubkey = context
.get_key_of_account_at_index(index as IndexOfAccount)
.map_err(|err| TransactionError::InstructionError(index as u8, err))?;
if !account.data().is_empty() {
let post_rent_state =
get_account_rent_state(rent, account.lamports(), account.data().len());
let pre_rent_state = self
.accounts
.get_account_ref(pubkey)
.map(|acc| get_account_rent_state(rent, acc.lamports(), acc.data().len()))
.unwrap_or(RentState::Uninitialized);
check_rent_state_with_account(
&pre_rent_state,
&post_rent_state,
pubkey,
index as IndexOfAccount,
)?;
}
}
}
Ok(())
}
fn execute_transaction_no_verify(
&mut self,
tx: VersionedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
map_sanitize_result(self.sanitize_transaction_no_verify(tx), |s_tx| {
self.execute_sanitized_transaction(&s_tx, log_collector)
})
}
fn execute_transaction(
&mut self,
tx: VersionedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
map_sanitize_result(self.sanitize_transaction(tx), |s_tx| {
self.execute_sanitized_transaction(&s_tx, log_collector)
})
}
fn execute_sanitized_transaction(
&mut self,
sanitized_tx: &SanitizedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
let CheckAndProcessTransactionSuccess {
core:
CheckAndProcessTransactionSuccessCore {
result,
compute_units_consumed,
context,
},
fee,
payer_key,
} = match self.check_and_process_transaction(sanitized_tx, log_collector) {
Ok(value) => value,
Err(value) => return value,
};
if let Some(ctx) = context {
let mut exec_result =
execution_result_if_context(sanitized_tx, ctx, result, compute_units_consumed, fee);
if let Some(payer) = payer_key.filter(|_| exec_result.tx_result.is_err()) {
exec_result.tx_result = self
.accounts
.withdraw(&payer, fee)
.and(exec_result.tx_result);
}
exec_result
} else {
ExecutionResult {
tx_result: result,
compute_units_consumed,
fee,
..Default::default()
}
}
}
fn execute_sanitized_transaction_readonly(
&self,
sanitized_tx: &SanitizedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
let CheckAndProcessTransactionSuccess {
core:
CheckAndProcessTransactionSuccessCore {
result,
compute_units_consumed,
context,
},
fee,
..
} = match self.check_and_process_transaction(sanitized_tx, log_collector) {
Ok(value) => value,
Err(value) => return value,
};
if let Some(ctx) = context {
execution_result_if_context(sanitized_tx, ctx, result, compute_units_consumed, fee)
} else {
ExecutionResult {
tx_result: result,
compute_units_consumed,
fee,
..Default::default()
}
}
}
fn check_and_process_transaction<'a, 'b>(
&'a self,
sanitized_tx: &'b SanitizedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> Result<CheckAndProcessTransactionSuccess<'b>, ExecutionResult>
where
'a: 'b,
{
self.maybe_blockhash_check(sanitized_tx)?;
let compute_budget_limits = get_compute_budget_limits(sanitized_tx, &self.feature_set)?;
self.maybe_history_check(sanitized_tx)?;
let (result, compute_units_consumed, context, fee, payer_key) =
self.process_transaction(sanitized_tx, compute_budget_limits, log_collector);
Ok(CheckAndProcessTransactionSuccess {
core: {
CheckAndProcessTransactionSuccessCore {
result,
compute_units_consumed,
context,
}
},
fee,
payer_key,
})
}
fn maybe_history_check(
&self,
sanitized_tx: &SanitizedTransaction,
) -> Result<(), ExecutionResult> {
if self.sigverify && self.history.check_transaction(sanitized_tx.signature()) {
return Err(ExecutionResult {
tx_result: Err(TransactionError::AlreadyProcessed),
..Default::default()
});
}
Ok(())
}
fn maybe_blockhash_check(
&self,
sanitized_tx: &SanitizedTransaction,
) -> Result<(), ExecutionResult> {
if self.blockhash_check {
self.check_transaction_age(sanitized_tx)?;
}
Ok(())
}
fn execute_transaction_readonly(
&self,
tx: VersionedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
map_sanitize_result(self.sanitize_transaction(tx), |s_tx| {
self.execute_sanitized_transaction_readonly(&s_tx, log_collector)
})
}
fn execute_transaction_no_verify_readonly(
&self,
tx: VersionedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> ExecutionResult {
map_sanitize_result(self.sanitize_transaction_no_verify(tx), |s_tx| {
self.execute_sanitized_transaction_readonly(&s_tx, log_collector)
})
}
pub fn send_transaction(&mut self, tx: impl Into<VersionedTransaction>) -> TransactionResult {
let log_collector = LogCollector {
bytes_limit: self.log_bytes_limit,
..Default::default()
};
let log_collector = Rc::new(RefCell::new(log_collector));
let vtx: VersionedTransaction = tx.into();
let ExecutionResult {
post_accounts,
tx_result,
signature,
compute_units_consumed,
inner_instructions,
return_data,
included,
fee,
} = if self.sigverify {
self.execute_transaction(vtx, log_collector.clone())
} else {
self.execute_transaction_no_verify(vtx, log_collector.clone())
};
let Ok(logs) = Rc::try_unwrap(log_collector).map(|lc| lc.into_inner().messages) else {
unreachable!("Log collector should not be used after send_transaction returns")
};
let meta = TransactionMetadata {
logs,
inner_instructions,
compute_units_consumed,
return_data,
signature,
fee,
};
if let Err(tx_err) = tx_result {
let err = TransactionResult::Err(FailedTransactionMetadata { err: tx_err, meta });
if included {
self.history.add_new_transaction(signature, err.clone());
}
err
} else {
self.history
.add_new_transaction(signature, Ok(meta.clone()));
self.accounts
.sync_accounts(post_accounts)
.expect("It shouldn't be possible to write invalid sysvars in send_transaction.");
TransactionResult::Ok(meta)
}
}
pub fn simulate_transaction(
&self,
tx: impl Into<VersionedTransaction>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
let log_collector = LogCollector {
bytes_limit: self.log_bytes_limit,
..Default::default()
};
let log_collector = Rc::new(RefCell::new(log_collector));
let ExecutionResult {
post_accounts,
tx_result,
signature,
compute_units_consumed,
inner_instructions,
return_data,
fee,
..
} = if self.sigverify {
self.execute_transaction_readonly(tx.into(), log_collector.clone())
} else {
self.execute_transaction_no_verify_readonly(tx.into(), log_collector.clone())
};
let Ok(logs) = Rc::try_unwrap(log_collector).map(|lc| lc.into_inner().messages) else {
unreachable!("Log collector should not be used after simulate_transaction returns")
};
let meta = TransactionMetadata {
signature,
logs,
inner_instructions,
compute_units_consumed,
return_data,
fee,
};
if let Err(tx_err) = tx_result {
Err(FailedTransactionMetadata { err: tx_err, meta })
} else {
Ok(SimulatedTransactionInfo {
meta,
post_accounts,
})
}
}
pub fn expire_blockhash(&mut self) {
self.latest_blockhash = create_blockhash(&self.latest_blockhash.to_bytes());
#[allow(deprecated)]
self.set_sysvar(&RecentBlockhashes::from_iter([IterItem(
0,
&self.latest_blockhash,
self.fee_structure.lamports_per_signature,
)]));
}
pub fn warp_to_slot(&mut self, slot: u64) {
let mut clock = self.get_sysvar::<Clock>();
clock.slot = slot;
self.set_sysvar(&clock);
}
pub fn get_compute_budget(&self) -> Option<ComputeBudget> {
self.compute_budget
}
pub fn get_sigverify(&self) -> bool {
self.sigverify
}
#[cfg(feature = "internal-test")]
pub fn get_feature_set(&self) -> Arc<FeatureSet> {
self.feature_set.clone().into()
}
fn check_transaction_age(&self, tx: &SanitizedTransaction) -> Result<(), ExecutionResult> {
self.check_transaction_age_inner(tx)
.map_err(|e| ExecutionResult {
tx_result: Err(e),
..Default::default()
})
}
fn check_transaction_age_inner(
&self,
tx: &SanitizedTransaction,
) -> solana_transaction_error::TransactionResult<()> {
let recent_blockhash = tx.message().recent_blockhash();
if recent_blockhash == &self.latest_blockhash
|| self.check_transaction_for_nonce(
tx,
&DurableNonce::from_blockhash(&self.latest_blockhash),
)
{
Ok(())
} else {
log::error!(
"Blockhash {} not found. Expected blockhash {}",
recent_blockhash,
self.latest_blockhash
);
Err(TransactionError::BlockhashNotFound)
}
}
fn check_message_for_nonce(&self, message: &SanitizedMessage) -> bool {
message
.get_durable_nonce()
.and_then(|nonce_address| self.accounts.get_account_ref(nonce_address))
.and_then(|nonce_account| {
solana_nonce_account::verify_nonce_account(
nonce_account,
message.recent_blockhash(),
)
})
.is_some_and(|nonce_data| {
message
.get_ix_signers(NONCED_TX_MARKER_IX_INDEX as usize)
.any(|signer| signer == &nonce_data.authority)
})
}
fn check_transaction_for_nonce(
&self,
tx: &SanitizedTransaction,
next_durable_nonce: &DurableNonce,
) -> bool {
let nonce_is_advanceable = tx.message().recent_blockhash() != next_durable_nonce.as_hash();
nonce_is_advanceable && self.check_message_for_nonce(tx.message())
}
#[cfg(feature = "invocation-inspect-callback")]
pub fn set_invocation_inspect_callback<C: InvocationInspectCallback + 'static>(
&mut self,
callback: C,
) {
self.invocation_inspect_callback = Arc::new(callback);
}
}
struct CheckAndProcessTransactionSuccessCore<'ix_data> {
result: Result<(), TransactionError>,
compute_units_consumed: u64,
context: Option<TransactionContext<'ix_data>>,
}
struct CheckAndProcessTransactionSuccess<'ix_data> {
core: CheckAndProcessTransactionSuccessCore<'ix_data>,
fee: u64,
payer_key: Option<Pubkey>,
}
fn execution_result_if_context(
sanitized_tx: &SanitizedTransaction,
ctx: TransactionContext,
result: Result<(), TransactionError>,
compute_units_consumed: u64,
fee: u64,
) -> ExecutionResult {
let (signature, return_data, inner_instructions, post_accounts) =
execute_tx_helper(sanitized_tx, ctx);
ExecutionResult {
tx_result: result,
signature,
post_accounts,
inner_instructions,
compute_units_consumed,
return_data,
included: true,
fee,
}
}
fn execute_tx_helper(
sanitized_tx: &SanitizedTransaction,
ctx: TransactionContext,
) -> (
Signature,
solana_transaction_context::TransactionReturnData,
InnerInstructionsList,
Vec<(Pubkey, AccountSharedData)>,
) {
let signature = sanitized_tx.signature().to_owned();
let inner_instructions = inner_instructions_list_from_instruction_trace(&ctx);
let ExecutionRecord {
accounts,
return_data,
touched_account_count: _,
accounts_resize_delta: _,
} = ctx.into();
let msg = sanitized_tx.message();
let post_accounts = accounts
.into_iter()
.enumerate()
.filter_map(|(idx, pair)| msg.is_writable(idx).then_some(pair))
.collect();
(signature, return_data, inner_instructions, post_accounts)
}
fn get_compute_budget_limits(
sanitized_tx: &SanitizedTransaction,
feature_set: &FeatureSet,
) -> Result<ComputeBudgetLimits, ExecutionResult> {
process_compute_budget_instructions(
SVMMessage::program_instructions_iter(sanitized_tx),
feature_set,
)
.map_err(|e| ExecutionResult {
tx_result: Err(e),
..Default::default()
})
}
fn validate_fee_payer(
payer_address: &Pubkey,
payer_account: &mut AccountSharedData,
payer_index: IndexOfAccount,
rent: &Rent,
fee: u64,
) -> solana_transaction_error::TransactionResult<()> {
if payer_account.lamports() == 0 {
error!("Payer account {payer_address} not found.");
return Err(TransactionError::AccountNotFound);
}
let system_account_kind = get_system_account_kind(payer_account).ok_or_else(|| {
error!("Payer account {payer_address} is not a system account");
TransactionError::InvalidAccountForFee
})?;
let min_balance = match system_account_kind {
SystemAccountKind::System => 0,
SystemAccountKind::Nonce => {
rent.minimum_balance(solana_nonce::state::State::size())
}
};
let payer_lamports = payer_account.lamports();
payer_lamports
.checked_sub(min_balance)
.and_then(|v| v.checked_sub(fee))
.ok_or_else(|| {
error!(
"Payer account {payer_address} has insufficient lamports for fee. Payer lamports: \
{payer_lamports} min_balance: {min_balance} fee: {fee}"
);
TransactionError::InsufficientFundsForFee
})?;
let payer_len = payer_account.data().len();
let payer_pre_rent_state = get_account_rent_state(rent, payer_account.lamports(), payer_len);
payer_account.checked_sub_lamports(fee).unwrap();
let payer_post_rent_state = get_account_rent_state(rent, payer_account.lamports(), payer_len);
check_rent_state_with_account(
&payer_pre_rent_state,
&payer_post_rent_state,
payer_address,
payer_index,
)
}
fn map_sanitize_result<F>(
res: Result<SanitizedTransaction, ExecutionResult>,
op: F,
) -> ExecutionResult
where
F: FnOnce(SanitizedTransaction) -> ExecutionResult,
{
match res {
Ok(s_tx) => op(s_tx),
Err(e) => e,
}
}
#[cfg(feature = "invocation-inspect-callback")]
pub trait InvocationInspectCallback: Send + Sync {
fn before_invocation(
&self,
tx: &SanitizedTransaction,
program_indices: &[IndexOfAccount],
invoke_context: &InvokeContext,
);
fn after_invocation(&self, invoke_context: &InvokeContext, enable_register_tracing: bool);
}
#[cfg(feature = "invocation-inspect-callback")]
pub struct EmptyInvocationInspectCallback;
#[cfg(feature = "invocation-inspect-callback")]
impl InvocationInspectCallback for EmptyInvocationInspectCallback {
fn before_invocation(&self, _: &SanitizedTransaction, _: &[IndexOfAccount], _: &InvokeContext) {
}
fn after_invocation(&self, _: &InvokeContext, _enable_register_tracing: bool) {}
}
#[cfg(test)]
mod tests {
use {
super::*,
solana_instruction::{account_meta::AccountMeta, Instruction},
solana_message::{Message, VersionedMessage},
};
#[test]
fn sysvar_accounts_are_demoted_to_readonly() {
let payer = Keypair::new();
let svm = LiteSVM::new();
let rent_key = solana_sdk_ids::sysvar::rent::id();
let ix = Instruction {
program_id: solana_sdk_ids::system_program::id(),
accounts: vec![AccountMeta {
pubkey: rent_key,
is_signer: false,
is_writable: true,
}],
data: vec![],
};
let message = Message::new(&[ix], Some(&payer.pubkey()));
let tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(message), &[&payer]).unwrap();
let sanitized = svm.sanitize_transaction_no_verify_inner(tx).unwrap();
assert!(!sanitized.message().is_writable(1));
}
}