use std::{cell::RefCell, marker::PhantomData, path::Path, rc::Rc, sync::Arc};
use agave_feature_set::{FeatureSet, raise_cpi_nesting_limit_to_8};
use agave_reserved_account_keys::ReservedAccountKeys;
use parking_lot::Mutex;
use serde::{Serialize, de::DeserializeOwned};
use solana_account::{
Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut,
};
use solana_address::Address;
use solana_builtins::BUILTINS;
use solana_clock::Clock;
use solana_compute_budget::compute_budget::ComputeBudget;
use solana_epoch_rewards::EpochRewards;
use solana_epoch_schedule::EpochSchedule;
use solana_feature_gate_interface::{self as feature_gate, Feature};
use solana_fee_structure::FeeStructure;
use solana_hash::Hash;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_last_restart_slot::LastRestartSlot;
use solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState};
use solana_message::{Message, SanitizedMessage, VersionedMessage};
use solana_native_token::LAMPORTS_PER_SOL;
use solana_nonce::{NONCED_TX_MARKER_IX_INDEX, state::DurableNonce};
use solana_nonce_account::verify_nonce_account;
use solana_program_runtime::{
invoke_context::BuiltinFunctionRegisterer, program_cache_entry::ProgramCacheEntry,
};
use solana_rent::Rent;
use solana_sdk_ids::{
bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, native_loader, system_program,
};
use solana_signature::Signature;
use solana_signer::Signer;
use solana_slot_hashes::{SlotHash, SlotHashes};
use solana_slot_history::SlotHistory;
use solana_stake_interface::stake_history::StakeHistory;
use solana_svm_log_collector::LogCollector;
use solana_svm_transaction::svm_message::{SVMMessage, SVMStaticMessage};
use solana_sysvar::Sysvar;
#[expect(deprecated)]
use solana_sysvar::{
fees::Fees,
recent_blockhashes::{IterItem, RecentBlockhashes},
};
use solana_sysvar_id::SysvarId;
use solana_transaction::{sanitized::SanitizedTransaction, versioned::VersionedTransaction};
use solana_transaction_error::TransactionError;
#[cfg(feature = "precompiles")]
use crate::precompile_registry::NativePrecompiles;
#[cfg(not(feature = "precompiles"))]
use crate::precompile_registry::NoPrecompiles;
#[cfg(feature = "precompiles")]
use crate::precompiles::load_precompiles;
#[cfg(feature = "register-tracing")]
use crate::register_tracing::DefaultRegisterTracingCallback;
use crate::{
AccountSource, AccountSourceError, AccountsView, BlockEnv, ClusterInfo, ClusterSpecId,
CustomSyscallRegistration, Hpsvm, HpsvmBuilder, InspectMode, Inspector, JitPolicy,
ProgramTierStats, RuntimeLimits, Svm, SvmTypesHost, TransactionOrigin, Version,
account_source::EmptyAccountSource,
batch::{
TransactionBatchError, TransactionBatchExecutionResult, TransactionBatchPlan,
plan_transaction_batch, send_transaction_batch,
},
commit::detached_into_result_and_delta,
error::HPSVMError,
executed::{DetachedTx, ExecutedTx, StagedFailure, staging_touches_caches},
helpers::*,
history::TransactionHistory,
instruction::InstructionCase,
precompile_registry::PrecompileProvider,
programs::{DEFAULT_PROGRAM_IDS, SPL_PROGRAM_IDS, load_default_programs, load_spl_programs},
runtime_registry::RuntimeExtensionRegistry,
tx_registry::{TxHandler, TxRegistryError, TxTag, tag_of},
types::{
AccountDiff, AccountSourceFailure, ExecutionDiagnostics, ExecutionResult, ExecutionTrace,
FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata,
TransactionResult,
},
utils::create_blockhash,
};
#[cfg(feature = "invocation-inspect-callback")]
use crate::{EmptyInvocationInspectCallback, InvocationInspectCallback};
fn sysvar_account_size<T>(sysvar_id: &Address) -> usize
where
T: Default + Serialize + SysvarId,
{
if solana_sysvar::recent_blockhashes::check_id(sysvar_id) {
solana_sysvar::recent_blockhashes::SIZE
} else if solana_sysvar::slot_hashes::check_id(sysvar_id) {
solana_sysvar::slot_hashes::SIZE
} else if solana_sysvar::slot_history::check_id(sysvar_id) {
solana_sysvar::slot_history::SIZE
} else {
AccountSharedData::new_data(0, &T::default(), &solana_sdk_ids::sysvar::id())
.map_or(0, |account| account.data_clone().len())
}
}
impl Hpsvm {
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn new() -> Self {
Self::builder()
.with_program_test_defaults()
.build()
.expect("standard Svm construction should remain infallible")
}
pub fn builder() -> HpsvmBuilder {
HpsvmBuilder::new()
}
#[cfg(feature = "register-tracing")]
pub fn new_debuggable(enable_register_tracing: bool) -> Self {
Self::builder()
.with_register_tracing(enable_register_tracing)
.with_program_test_defaults()
.build()
.expect("standard debuggable Svm construction should remain infallible")
}
}
impl<'a, T: SvmTypesHost> Svm<'a, T> {
pub(crate) fn default_register_tracing_enabled() -> bool {
#[cfg(feature = "register-tracing")]
{
std::env::var("SBF_TRACE_DIR").is_ok()
}
#[cfg(not(feature = "register-tracing"))]
{
false
}
}
pub(crate) fn sync_block_env_slot(&mut self) {
self.block_env.slot = self.accounts.current_slot();
}
pub(crate) fn new_inner(_enable_register_tracing: bool) -> Self {
let feature_set = FeatureSet::default();
let latest_blockhash = create_blockhash(b"genesis");
Self {
accounts: Default::default(),
airdrop_kp: Keypair::new().to_bytes(),
builtins_loaded: false,
default_programs_loaded: false,
spl_programs_loaded: false,
reserved_account_keys: Self::reserved_account_keys_for_feature_set(&feature_set),
version: Version {
feature_set: Arc::new(feature_set),
sigverify: false,
blockhash_check: false,
fee_structure: FeeStructure::default(),
compute_diagnostics: true,
compute_budget: None,
rent: Rent::default(),
limits: RuntimeLimits::default(),
cluster: ClusterInfo {
spec_id: ClusterSpecId::MainnetBeta,
name: ClusterSpecId::MainnetBeta.name(),
},
jit_policy: JitPolicy::DEFAULT,
},
feature_accounts_loaded: false,
inspector: Mutex::new(None),
inspection_origin: TransactionOrigin::User,
runtime_registry: RuntimeExtensionRegistry::default(),
registry: crate::tx_registry::TxRegistry::with_defaults(),
block_env: BlockEnv {
latest_blockhash,
slot: 0,
ext: T::BlockEnvExt::default(),
},
history: TransactionHistory::new(),
sysvars_loaded: false,
#[cfg(feature = "precompiles")]
precompiles: Arc::new(NativePrecompiles::new()),
#[cfg(not(feature = "precompiles"))]
precompiles: Arc::new(NoPrecompiles),
tier_promoter: Arc::new(crate::programs::TierPromoter::new(JitPolicy::DEFAULT)),
_type_family: PhantomData,
#[cfg(feature = "invocation-inspect-callback")]
enable_register_tracing: _enable_register_tracing,
#[cfg(feature = "invocation-inspect-callback")]
invocation_inspect_callback: {
#[cfg(feature = "register-tracing")]
if _enable_register_tracing {
Arc::new(DefaultRegisterTracingCallback::default())
} else {
Arc::new(EmptyInvocationInspectCallback {})
}
#[cfg(not(feature = "register-tracing"))]
Arc::new(EmptyInvocationInspectCallback {})
},
}
}
pub fn new_with_defaults() -> Self {
let mut svm = Self::new_inner(false);
svm.set_feature_set(FeatureSet::all_enabled())
.expect("enabling every feature should succeed on a fresh VM");
svm.set_builtins();
svm.set_lamports(1_000_000 * LAMPORTS_PER_SOL);
svm.set_sysvars();
svm.set_feature_accounts();
svm.set_default_programs();
#[cfg(feature = "precompiles")]
svm.set_precompiles();
svm.set_sigverify(true);
svm.set_blockhash_check(true);
svm
}
pub fn with_inspector<I: Inspector<T> + 'a>(mut self, inspector: I) -> Self {
self.set_inspector(inspector);
self
}
pub fn set_inspector(&mut self, inspector: impl Inspector<T> + 'a) {
self.set_boxed_inspector(Box::new(inspector));
}
pub(crate) fn set_boxed_inspector(&mut self, mut inspector: Box<dyn Inspector<T> + 'a>) {
inspector.initialize(self);
*self.inspector.lock() = Some(inspector);
}
pub fn clear_inspector(&mut self) -> Option<Box<dyn Inspector<T> + 'a>> {
self.inspector.lock().take()
}
#[inline]
pub(crate) fn inspector_live(&self) -> bool {
let origin = self.inspection_origin;
self.inspector.lock().as_ref().is_some_and(|inspector| inspector.accepts_origin(origin))
}
fn fire_inspector(&self, op: impl FnOnce(&mut (dyn Inspector<T> + '_), &Self)) {
let Some(mut inspector) = self.inspector.lock().take() else { return };
let outcome =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| op(&mut *inspector, self)));
*self.inspector.lock() = Some(inspector);
if let Err(payload) = outcome {
std::panic::resume_unwind(payload);
}
}
pub(crate) fn on_transaction_start<const MODE: InspectMode>(&self, tx: &SanitizedTransaction) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, svm| inspector.transaction_start(svm, tx));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn on_instruction_start<const MODE: InspectMode>(
&self,
index: usize,
program_id: &Address,
) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, _| inspector.instruction_start(index, program_id));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn on_instruction_end<const MODE: InspectMode>(
&self,
index: usize,
program_id: &Address,
) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, _| inspector.instruction_end(index, program_id));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn on_log<const MODE: InspectMode>(&self, message: &str) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, _| inspector.log(message));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn on_account_change<const MODE: InspectMode>(
&self,
change: crate::state_change_sink::AccountChange<'_>,
) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, _| inspector.account_change(change));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn on_transaction_end<const MODE: InspectMode>(
&self,
result: &solana_transaction_error::TransactionResult<()>,
) {
match MODE {
InspectMode::DynInspector => {
self.fire_inspector(|inspector, svm| inspector.transaction_end(svm, result));
}
InspectMode::NoInspector => {}
}
}
pub(crate) fn with_transaction_origin<O>(
&mut self,
origin: TransactionOrigin,
op: impl FnOnce(&mut Self) -> O,
) -> O {
let previous_origin = self.inspection_origin;
self.inspection_origin = origin;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| op(self)));
self.inspection_origin = previous_origin;
match result {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}
pub(crate) fn with_temporary_account_source<O>(
&mut self,
source: Arc<dyn AccountSource>,
op: impl FnOnce(&mut Self) -> O,
) -> O {
let previous_source = self.accounts.replace_account_source(source);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| op(self)));
self.accounts.set_account_source(previous_source);
match result {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}
pub(crate) fn clear_feature_accounts(&mut self, previous_feature_set: &FeatureSet) {
previous_feature_set.active().iter().for_each(|(feature_id, _)| {
self.accounts.remove_account(feature_id);
});
}
pub(crate) fn clear_builtin_accounts(&mut self) {
for builtin in BUILTINS {
self.accounts.remove_account(&builtin.program_id);
}
}
pub(crate) fn clear_default_programs(&mut self) {
for program_id in &DEFAULT_PROGRAM_IDS {
if self
.accounts
.get_account_ref(program_id)
.is_some_and(|account| account.owner() == &bpf_loader_upgradeable::id())
{
self.accounts.remove_account(&get_program_data_address(program_id));
}
self.accounts.remove_account(program_id);
}
}
pub(crate) fn clear_spl_programs(&mut self) {
for program_id in &SPL_PROGRAM_IDS {
if self
.accounts
.get_account_ref(program_id)
.is_some_and(|account| account.owner() == &bpf_loader_upgradeable::id())
{
self.accounts.remove_account(&get_program_data_address(program_id));
}
self.accounts.remove_account(program_id);
}
}
#[cfg(feature = "precompiles")]
pub(crate) fn clear_precompile_accounts(&mut self) {
for program_id in self.precompiles.program_ids() {
self.accounts.remove_account(&program_id);
}
}
pub(crate) fn reconfigure_materialized_feature_state(
&mut self,
previous_feature_set: &FeatureSet,
) -> Result<(), HPSVMError> {
if self.feature_accounts_loaded {
self.clear_feature_accounts(previous_feature_set);
self.materialize_feature_accounts();
}
if self.default_programs_loaded {
self.clear_default_programs();
}
if self.spl_programs_loaded {
self.clear_spl_programs();
}
if self.builtins_loaded {
self.clear_builtin_accounts();
self.load_builtins();
} else if !self.runtime_registry.custom_syscalls().is_empty() {
self.refresh_runtime_environments();
}
#[cfg(feature = "precompiles")]
if self.runtime_registry.loads_standard_precompiles() {
self.clear_precompile_accounts();
self.load_precompiles();
}
if self.default_programs_loaded {
self.load_default_programs();
}
if self.spl_programs_loaded {
self.load_spl_programs();
}
self.sync_jit_policy();
self.accounts.rebuild_program_cache().map_err(HPSVMError::from)
}
pub const fn set_compute_budget(&mut self, compute_budget: ComputeBudget) {
self.version.compute_budget = Some(compute_budget);
}
pub fn set_compute_unit_limit(&mut self, compute_unit_limit: u64) {
let mut compute_budget = self.version.compute_budget.unwrap_or_else(|| {
ComputeBudget::new_with_defaults(
self.version.feature_set.is_active(&raise_cpi_nesting_limit_to_8::ID),
)
});
compute_budget.compute_unit_limit = compute_unit_limit;
self.set_compute_budget(compute_budget);
}
pub const fn set_sigverify(&mut self, sigverify: bool) {
self.version.sigverify = sigverify;
}
pub const fn set_blockhash_check(&mut self, check: bool) {
self.version.blockhash_check = check;
}
pub const fn set_compute_diagnostics(&mut self, enabled: bool) {
self.version.compute_diagnostics = enabled;
}
pub fn set_jit_policy(&mut self, policy: JitPolicy) {
self.version.jit_policy = policy;
self.accounts.sync_jit_policy(policy);
self.tier_promoter = Arc::new(crate::programs::TierPromoter::new(policy));
}
#[must_use]
pub fn with_jit_policy(mut self, policy: JitPolicy) -> Self {
self.set_jit_policy(policy);
self
}
pub(crate) fn sync_jit_policy(&mut self) {
let policy = self.version.jit_policy;
if self.accounts.jit_policy() != policy {
self.accounts.sync_jit_policy(policy);
self.tier_promoter = Arc::new(crate::programs::TierPromoter::new(policy));
}
}
#[must_use]
pub fn program_tier_stats(&self) -> ProgramTierStats {
let promotion = self.tier_promoter.stats_handle();
ProgramTierStats::collect(
self.version.jit_policy,
&self.accounts,
crate::programs::program_entry_store().len(),
&promotion,
)
}
pub(crate) fn observe_tier(&self) {
self.tier_promoter.observe(&self.accounts);
}
pub fn prewarm_programs(&mut self, program_ids: &[Address]) -> Result<(), HPSVMError> {
self.sync_jit_policy();
self.accounts.prewarm_programs(program_ids).map_err(HPSVMError::from)?;
if self.version.jit_policy.is_lazy() {
let entries: Vec<_> = {
let cache = self.accounts.shared_programs_cache();
let guard = cache.read();
program_ids
.iter()
.filter_map(|program_id| guard.find(program_id))
.filter(|entry| !crate::programs::has_compiled_program(entry))
.collect()
};
for entry in entries {
if self.version.jit_policy.hot_threshold() == Some(0) {
self.tier_promoter.promote_blocking(entry);
} else {
self.tier_promoter.promote(entry);
}
}
}
Ok(())
}
pub(crate) fn set_sysvars(&mut self) {
self.sysvars_loaded = true;
self.set_sysvar_internal(&Clock::default());
self.set_sysvar_internal(&EpochRewards::default());
self.set_sysvar_internal(&EpochSchedule::default());
#[expect(deprecated)]
let fees = Fees::default();
self.set_sysvar_internal(&fees);
self.set_sysvar_internal(&LastRestartSlot::default());
let latest_blockhash = self.block_env.latest_blockhash;
#[expect(deprecated)]
self.set_sysvar_internal(&RecentBlockhashes::from_iter([IterItem(
0,
&latest_blockhash,
fees.fee_calculator.lamports_per_signature,
)]));
#[expect(deprecated)]
{
let mut rent_account = Rent::default();
if self
.version
.feature_set
.is_active(&agave_feature_set::deprecate_rent_exemption_threshold::id())
{
rent_account.exemption_threshold = 1.0f64.to_le_bytes();
rent_account.lamports_per_byte = solana_rent::DEFAULT_LAMPORTS_PER_BYTE;
}
self.set_sysvar_internal(&rent_account);
}
self.set_sysvar_internal(&SlotHashes::new(&[SlotHash::new(
self.accounts.current_slot(),
latest_blockhash,
)]));
self.set_sysvar_internal(&SlotHistory::default());
{
let account = AccountSharedData::new_data(
1,
&StakeHistory::default(),
&solana_sdk_ids::sysvar::id(),
)
.expect("StakeHistory account creation");
self.accounts
.add_account(StakeHistory::id(), account)
.expect("add StakeHistory account");
}
}
pub fn set_feature_set(&mut self, feature_set: FeatureSet) -> Result<(), HPSVMError> {
let previous_feature_set = Arc::clone(&self.version.feature_set);
let previous_accounts = self.accounts.clone();
let previous_reserved_account_keys = self.reserved_account_keys.clone();
self.version.feature_set = Arc::new(feature_set);
self.reserved_account_keys =
Self::reserved_account_keys_for_feature_set(&self.version.feature_set);
if let Err(error) = self.reconfigure_materialized_feature_state(&previous_feature_set) {
self.version.feature_set = previous_feature_set;
self.reserved_account_keys = previous_reserved_account_keys;
self.accounts = previous_accounts;
self.sync_jit_policy();
if let Err(rebuild_error) = self.accounts.rebuild_program_cache() {
tracing::error!(
?rebuild_error,
"failed to restore program cache after set_feature_set rollback"
);
}
return Err(error);
}
Ok(())
}
pub(crate) fn materialize_feature_accounts(&mut self) {
self.feature_accounts_loaded = true;
for (feature_id, activation_slot) in self.version.feature_set.active() {
let feature_account = Feature { activated_at: Some(*activation_slot) };
let lamports = self.minimum_balance_for_rent_exemption(Feature::size_of());
let account = feature_gate::create_account(&feature_account, lamports);
self.accounts.add_account_no_checks(*feature_id, account);
}
}
pub(crate) fn set_feature_accounts(&mut self) {
self.materialize_feature_accounts();
}
pub(crate) 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
}
pub(crate) fn load_builtins(&mut self) {
self.builtins_loaded = true;
self.refresh_runtime_environments();
for builtint in BUILTINS {
if builtint.enable_feature_id.is_none_or(|x| self.version.feature_set.is_active(&x)) {
let loaded_program = ProgramCacheEntry::new_builtin(builtint.register_fn);
self.accounts
.replenish_program_cache(builtint.program_id, Arc::new(loaded_program));
self.accounts.add_builtin_account(
builtint.program_id,
crate::utils::create_loadable_account_for_test(builtint.name),
);
}
}
}
pub(crate) fn set_builtins(&mut self) {
self.load_builtins();
}
pub fn set_lamports(&mut self, lamports: u64) {
self.accounts.add_account_no_checks(
Keypair::try_from(self.airdrop_kp.as_slice())
.expect("airdrop keypair should be valid")
.pubkey(),
AccountSharedData::new(lamports, 0, &system_program::id()),
);
}
pub(crate) fn load_default_programs(&mut self) {
self.default_programs_loaded = true;
load_default_programs(self);
}
pub(crate) fn load_spl_programs(&mut self) {
self.spl_programs_loaded = true;
load_spl_programs(self);
}
pub(crate) fn set_default_programs(&mut self) {
self.load_default_programs();
}
pub(crate) fn set_spl_programs(&mut self) {
self.load_spl_programs();
}
pub fn set_transaction_history(&mut self, capacity: usize) {
self.history.set_capacity(capacity);
}
pub fn set_account_source(&mut self, source: impl AccountSource + 'static) {
self.accounts.set_account_source(Arc::new(source));
}
pub const fn set_log_bytes_limit(&mut self, limit: Option<usize>) {
self.version.limits.log_bytes_limit = limit;
}
#[cfg(feature = "precompiles")]
pub(crate) fn load_precompiles(&mut self) {
load_precompiles(self);
}
#[cfg(feature = "precompiles")]
pub(crate) fn set_precompiles(&mut self) {
self.runtime_registry.enable_standard_precompiles();
self.load_precompiles();
}
pub fn set_precompile_provider(&mut self, provider: Arc<dyn PrecompileProvider<T>>) {
self.precompiles = provider;
#[cfg(feature = "precompiles")]
self.load_precompiles();
}
pub fn precompile_provider(&self) -> Arc<dyn PrecompileProvider<T>> {
Arc::clone(&self.precompiles)
}
pub fn minimum_balance_for_rent_exemption(&self, data_len: usize) -> u64 {
self.accounts.minimum_balance_for_rent_exemption(data_len)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_account(&self, address: &Address) -> Option<Account> {
self.accounts.get_account(address).map(Into::into)
}
pub fn try_get_account(
&self,
address: &Address,
) -> Result<Option<Account>, AccountSourceError> {
self.accounts.try_get_account(address).map(|account| account.map(Into::into))
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn set_account(&mut self, address: Address, data: Account) -> Result<(), HPSVMError> {
self.sync_jit_policy();
self.accounts.add_account(address, data.into())?;
self.sync_block_env_slot();
Ok(())
}
pub const fn accounts(&self) -> AccountsView<'_> {
AccountsView::new(&self.accounts)
}
pub fn get_balance(&self, address: &Address) -> Option<u64> {
self.accounts.get_account(address).map(|account| account.lamports())
}
pub const fn latest_blockhash(&self) -> Hash {
self.block_env.latest_blockhash
}
pub const fn block_env(&self) -> BlockEnv<T::BlockEnvExt> {
self.block_env
}
pub fn set_block_env_ext(&mut self, ext: T::BlockEnvExt) {
self.block_env.ext = ext;
}
#[inline]
pub fn version(&self) -> &Version {
&self.version
}
pub fn set_sysvar<S>(&mut self, sysvar: &S) -> Result<(), HPSVMError>
where
S: Default + Serialize + SysvarId,
{
self.try_set_sysvar(sysvar)?;
self.sync_block_env_slot();
Ok(())
}
pub(crate) fn try_set_sysvar<S>(&mut self, sysvar: &S) -> Result<(), HPSVMError>
where
S: Default + Serialize + SysvarId,
{
let mut account = AccountSharedData::new(
1,
sysvar_account_size::<S>(&S::id()),
&solana_sdk_ids::sysvar::id(),
);
account.serialize_data(sysvar).map_err(|error| HPSVMError::SysvarSerialization {
sysvar: std::any::type_name::<S>(),
reason: error.to_string(),
})?;
self.accounts.add_account(S::id(), account)
}
pub(crate) fn set_sysvar_internal<S>(&mut self, sysvar: &S)
where
S: Default + Serialize + SysvarId,
{
self.try_set_sysvar(sysvar)
.expect("internal sysvar setup should never fail for supported sysvars");
self.sync_block_env_slot();
}
pub fn get_sysvar<S>(&self) -> S
where
S: Sysvar + SysvarId + DeserializeOwned,
{
self.try_get_sysvar().expect("sysvar account should exist and deserialize")
}
pub(crate) fn try_get_sysvar<S>(&self) -> Result<S, HPSVMError>
where
S: Sysvar + SysvarId + DeserializeOwned,
{
let account = self
.accounts
.get_account_ref(&S::id())
.ok_or(HPSVMError::MissingRuntimeComponent { component: "sysvars" })?;
account.deserialize_data().map_err(|error| HPSVMError::SysvarSerialization {
sysvar: std::any::type_name::<S>(),
reason: error.to_string(),
})
}
pub(crate) fn require_sysvars_loaded(&self) -> Result<(), HPSVMError> {
if self.sysvars_loaded {
Ok(())
} else {
Err(HPSVMError::MissingRuntimeComponent { component: "sysvars" })
}
}
pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> {
self.history.get_transaction(signature)
}
pub fn airdrop_pubkey(&self) -> Address {
Keypair::try_from(self.airdrop_kp.as_slice())
.expect("airdrop keypair should be valid")
.pubkey()
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn airdrop(&mut self, address: &Address, lamports: u64) -> TransactionResult {
let payer =
Keypair::try_from(self.airdrop_kp.as_slice()).expect("airdrop keypair should be valid");
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(Message::new_with_blockhash(
&[solana_system_interface::instruction::transfer(
&payer.pubkey(),
address,
lamports,
)],
Some(&payer.pubkey()),
&self.block_env.latest_blockhash,
)),
&[payer],
)
.expect("failed to create airdrop transaction");
self.with_temporary_account_source(Arc::new(EmptyAccountSource), |svm| {
svm.with_transaction_origin(TransactionOrigin::InternalAirdrop, |svm| {
svm.send_transaction(tx)
})
})
}
pub fn add_builtin(&mut self, program_id: Address, entrypoint: BuiltinFunctionRegisterer) {
let builtin = ProgramCacheEntry::new_builtin(entrypoint);
self.accounts.replenish_program_cache(program_id, Arc::new(builtin));
let mut account = AccountSharedData::new(1, 1, &native_loader::id());
account.set_executable(true);
self.accounts.add_account_no_checks(program_id, account);
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn add_program_from_file(
&mut self,
program_id: impl Into<Address>,
path: impl AsRef<Path>,
) -> Result<(), HPSVMError> {
let bytes = std::fs::read(path)?;
self.add_program(program_id, &bytes)?;
Ok(())
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn add_program_internal<const PREVERIFIED: bool, const CACHED: bool>(
&mut self,
program_id: impl Into<Address>,
program_bytes: &[u8],
loader_id: &Address,
) -> Result<(), HPSVMError> {
self.sync_jit_policy();
let program_id = program_id.into();
let current_slot = self.accounts.current_slot();
if bpf_loader_upgradeable::check_id(loader_id) {
let (programdata_address, _bump) =
Address::find_program_address(&[program_id.as_ref()], loader_id);
let programdata_metadata_len = UpgradeableLoaderState::size_of_programdata_metadata();
let programdata_len = programdata_metadata_len + program_bytes.len();
let programdata_lamports = self.minimum_balance_for_rent_exemption(programdata_len);
let mut programdata_account =
AccountSharedData::new(programdata_lamports, programdata_len, loader_id);
programdata_account
.set_state(&UpgradeableLoaderState::ProgramData {
slot: current_slot,
upgrade_authority_address: None,
})
.expect("UpgradeableLoaderState::ProgramData serialization should never fail");
programdata_account.data_as_mut_slice()[programdata_metadata_len..]
.copy_from_slice(program_bytes);
let program_len = UpgradeableLoaderState::size_of_program();
let program_lamports = self.minimum_balance_for_rent_exemption(program_len);
let mut program_account =
AccountSharedData::new(program_lamports, program_len, loader_id);
program_account.set_executable(true);
program_account
.set_state(&UpgradeableLoaderState::Program { programdata_address })
.expect("UpgradeableLoaderState::Program serialization should never fail");
self.accounts.add_account_no_checks(programdata_address, programdata_account);
self.accounts.add_account_no_checks(program_id, program_account);
} else if bpf_loader::check_id(loader_id) || bpf_loader_deprecated::check_id(loader_id) {
let program_len = program_bytes.len();
let lamports = self.minimum_balance_for_rent_exemption(program_len);
let mut account = AccountSharedData::new(lamports, program_len, loader_id);
account.set_executable(true);
account.set_data_from_slice(program_bytes);
self.accounts.add_account_no_checks(program_id, account);
} else {
return Err(HPSVMError::InvalidLoader { program_id, loader_id: *loader_id });
};
let env = self.accounts.runtime_environments().get_env_for_execution();
let loaded_program_arc =
self.resolve_program_entry::<CACHED>(loader_id, env, current_slot, program_bytes)?;
self.accounts.replenish_program_cache(program_id, loaded_program_arc);
Ok(())
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn add_program(
&mut self,
program_id: impl Into<Address>,
program_bytes: &[u8],
) -> Result<(), HPSVMError> {
self.add_program_internal::<false, false>(
program_id,
program_bytes,
&bpf_loader_upgradeable::id(),
)?;
Ok(())
}
pub fn add_program_with_loader(
&mut self,
program_id: impl Into<Address>,
program_bytes: &[u8],
loader_id: Address,
) -> Result<(), HPSVMError> {
self.add_program_internal::<false, false>(program_id, program_bytes, &loader_id)?;
Ok(())
}
pub(crate) fn add_program_preverified(
&mut self,
program_id: impl Into<Address>,
program_bytes: &[u8],
loader_id: &Address,
) -> Result<(), HPSVMError> {
self.add_program_internal::<true, true>(program_id, program_bytes, loader_id)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn send_transaction(&mut self, tx: impl Into<VersionedTransaction>) -> TransactionResult {
match Self::transact_core(self, tx.into()) {
Ok(staged) => staged.commit(),
Err(failure) => failure.into_result(),
}
}
pub fn process_instruction_case(
&self,
case: &InstructionCase,
) -> Result<DetachedTx, HPSVMError> {
let mut working = self.clone();
working.set_sigverify(false);
for (address, account) in &case.pre_accounts {
working.set_account(*address, account.clone())?;
}
let fee_payer = fee_payer_for_instruction_case(case);
if working.get_account(&fee_payer).is_none() {
working.set_account(
fee_payer,
Account {
lamports: 1_000_000_000,
owner: system_program::id(),
..Default::default()
},
)?;
}
let message = Message::new_with_blockhash(
&[case.instruction()],
Some(&fee_payer),
&working.latest_blockhash(),
);
let signatures =
vec![Signature::default(); usize::from(message.header.num_required_signatures)];
let tx = VersionedTransaction { signatures, message: VersionedMessage::Legacy(message) };
working.try_transact(tx)
}
pub fn process_instruction(&mut self, instruction: Instruction) -> TransactionResult {
self.process_instruction_chain([instruction])
}
pub fn process_instruction_with_accounts(
&mut self,
instruction: Instruction,
pre_accounts: impl IntoIterator<Item = (Address, Account)>,
) -> Result<TransactionMetadata, FailedTransactionMetadata> {
self.process_instruction_chain_with_accounts([instruction], pre_accounts)
}
pub fn process_instruction_chain(
&mut self,
instructions: impl IntoIterator<Item = Instruction>,
) -> TransactionResult {
let tx = self.instruction_chain_transaction(&instructions.into_iter().collect::<Vec<_>>());
let sigverify = self.version.sigverify;
self.version.sigverify = false;
let outcome = Self::transact_core(self, tx);
let result = match outcome {
Ok(staged) => staged.commit(),
Err(failure) => failure.into_result(),
};
self.version.sigverify = sigverify;
result
}
pub fn process_instruction_chain_with_accounts(
&mut self,
instructions: impl IntoIterator<Item = Instruction>,
pre_accounts: impl IntoIterator<Item = (Address, Account)>,
) -> TransactionResult {
for (address, account) in pre_accounts {
if self.set_account(address, account).is_err() {
return Err(FailedTransactionMetadata {
err: TransactionError::InstructionError(
0,
solana_transaction::InstructionError::InvalidAccountData,
),
meta: TransactionMetadata::default(),
});
}
}
self.process_instruction_chain(instructions)
}
pub fn simulate_instruction(
&self,
instruction: Instruction,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
self.simulate_instruction_chain([instruction])
}
pub fn simulate_instruction_with_accounts(
&self,
instruction: Instruction,
pre_accounts: impl IntoIterator<Item = (Address, Account)>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
self.simulate_instruction_chain_with_accounts([instruction], pre_accounts)
}
pub fn simulate_instruction_chain(
&self,
instructions: impl IntoIterator<Item = Instruction>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
let mut working = self.clone();
working.version.sigverify = false;
let tx =
working.instruction_chain_transaction(&instructions.into_iter().collect::<Vec<_>>());
let detached = working.transact_detached(tx);
if let Err(tx_err) = detached.status {
Err(FailedTransactionMetadata { err: tx_err, meta: detached.meta })
} else {
Ok(SimulatedTransactionInfo {
meta: detached.meta,
post_accounts: detached.post_accounts,
})
}
}
pub fn simulate_instruction_chain_with_accounts(
&self,
instructions: impl IntoIterator<Item = Instruction>,
pre_accounts: impl IntoIterator<Item = (Address, Account)>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
let mut working = self.clone();
for (address, account) in pre_accounts {
if working.set_account(address, account).is_err() {
return Err(FailedTransactionMetadata {
err: TransactionError::InstructionError(
0,
solana_transaction::InstructionError::InvalidAccountData,
),
meta: TransactionMetadata::default(),
});
}
}
working.version.sigverify = false;
let tx =
working.instruction_chain_transaction(&instructions.into_iter().collect::<Vec<_>>());
let detached = working.transact_detached(tx);
if let Err(tx_err) = detached.status {
Err(FailedTransactionMetadata { err: tx_err, meta: detached.meta })
} else {
Ok(SimulatedTransactionInfo {
meta: detached.meta,
post_accounts: detached.post_accounts,
})
}
}
pub fn try_transact(
&self,
tx: impl Into<VersionedTransaction>,
) -> Result<DetachedTx, HPSVMError> {
let mut detached = self.dispatch_detached(tx.into()).map_err(HPSVMError::from)?;
if let Some(error) = detached.take_fatal() {
return Err(error);
}
Ok(detached)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn transact<'svm>(
&'svm mut self,
tx: impl Into<VersionedTransaction>,
) -> Result<ExecutedTx<'svm, 'a, T>, HPSVMError> {
Self::transact_core(self, tx.into()).map_err(StagedFailure::into_error)
}
pub fn transact_detached(&self, tx: impl Into<VersionedTransaction>) -> DetachedTx {
let tx = tx.into();
let signature = tx.signatures.first().copied().unwrap_or_default();
match self.dispatch_detached(tx) {
Ok(detached) => detached,
Err(error) => DetachedTx::for_dispatch_failure(&error, signature),
}
}
pub fn register_tx_handler(
&mut self,
tag: TxTag,
handle: impl TxHandler<T, VersionedTransaction, DetachedTx> + 'static,
) {
self.registry.register(tag, handle);
}
pub(crate) fn dispatch_detached(
&self,
tx: VersionedTransaction,
) -> Result<DetachedTx, TxRegistryError> {
let tag = tag_of(&tx);
self.registry.dispatch(tag, tx, self)
}
pub(crate) fn default_execute_detached(&self, tx: VersionedTransaction) -> DetachedTx {
let (execution, log_collector) = self.execute_readonly(tx);
execution_into_detached(self, execution, log_collector, "tx_registry::default_execute")
}
fn transact_core<'svm, 'host>(
svm: &'svm mut Svm<'host, T>,
tx: VersionedTransaction,
) -> Result<ExecutedTx<'svm, 'host, T>, StagedFailure> {
svm.sync_jit_policy();
let signature = tx.signatures.first().copied().unwrap_or_default();
let mut detached = match svm.dispatch_detached(tx) {
Ok(detached) => detached,
Err(error) => {
let detached = DetachedTx::for_dispatch_failure(&error, signature);
let hpsvm_error = HPSVMError::from(error);
let (result, _) = detached_into_result_and_delta(detached, false);
return Err(StagedFailure::new(hpsvm_error, result));
}
};
if let Some(error) = detached.take_fatal() {
let (result, _) = detached_into_result_and_delta(detached, false);
return Err(StagedFailure::new(error, result));
}
let checkpoint = svm.accounts.checkpoint();
let inspector_live = detached.inspector_live;
let cache_dirty = staging_touches_caches(&detached.post_accounts, &svm.accounts);
let history_enabled = svm.history.is_enabled();
let (result, delta) = crate::hotpath_block!(
"hpsvm::commit::detached_into_result_and_delta",
detached_into_result_and_delta(detached, history_enabled)
);
let (post_accounts, history_entry) = delta.into_parts();
let changes: Vec<crate::state_change_sink::AccountChangeRecord> = post_accounts
.iter()
.map(|(address, after)| crate::state_change_sink::AccountChangeRecord {
address: *address,
before: svm.accounts.get_account(address),
after: if after.lamports() == 0 { None } else { Some(after.clone()) },
})
.collect();
if let Err(error) = crate::hotpath_block!(
"hpsvm::commit::sync_staged_accounts",
svm.accounts.sync_accounts(post_accounts)
) {
svm.accounts.rollback(checkpoint);
return Err(StagedFailure::new(error, result));
}
Ok(ExecutedTx::new(
svm,
checkpoint,
result,
history_entry,
cache_dirty,
changes,
inspector_live,
))
}
pub(crate) fn execute_readonly(
&self,
tx: VersionedTransaction,
) -> (ExecutionResult, Rc<RefCell<LogCollector>>) {
let log_collector = Rc::new(RefCell::new(LogCollector {
bytes_limit: self.version.limits.log_bytes_limit,
..Default::default()
}));
let output = if self.inspector_live() {
self.execute_readonly_impl::<{ InspectMode::DynInspector }>(tx, log_collector)
} else {
self.execute_readonly_impl::<{ InspectMode::NoInspector }>(tx, log_collector)
};
self.observe_tier();
output
}
fn execute_readonly_impl<const MODE: InspectMode>(
&self,
tx: VersionedTransaction,
log_collector: Rc<RefCell<LogCollector>>,
) -> (ExecutionResult, Rc<RefCell<LogCollector>>) {
let execution = if self.version.sigverify {
self.execute_transaction_readonly::<MODE>(tx, log_collector.clone())
} else {
self.execute_transaction_no_verify_readonly::<MODE>(tx, log_collector.clone())
};
(execution, log_collector)
}
pub fn plan_transaction_batch<V>(
&self,
txs: impl IntoIterator<Item = V>,
) -> Result<TransactionBatchPlan, TransactionBatchError>
where
V: Into<VersionedTransaction>,
{
let transactions = txs.into_iter().map(Into::into).collect::<Vec<_>>();
plan_transaction_batch(self, &transactions)
}
pub fn send_transaction_batch<V>(
&mut self,
txs: impl IntoIterator<Item = V>,
) -> Result<TransactionBatchExecutionResult, TransactionBatchError>
where
V: Into<VersionedTransaction>,
{
let transactions = txs.into_iter().map(Into::into).collect::<Vec<_>>();
send_transaction_batch(self, transactions)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn simulate_transaction(
&self,
tx: impl Into<VersionedTransaction>,
) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
let detached = self.transact_detached(tx);
if let Err(tx_err) = detached.status {
Err(FailedTransactionMetadata { err: tx_err, meta: detached.meta })
} else {
Ok(SimulatedTransactionInfo {
meta: detached.meta,
post_accounts: detached.post_accounts,
})
}
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn expire_blockhash(&mut self) {
self.block_env.latest_blockhash =
create_blockhash(&self.block_env.latest_blockhash.to_bytes());
#[expect(deprecated)]
self.set_sysvar_internal(&RecentBlockhashes::from_iter([IterItem(
0,
&self.block_env.latest_blockhash,
self.version.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_internal(&clock);
}
pub const fn get_compute_budget(&self) -> Option<ComputeBudget> {
self.version.compute_budget
}
pub const fn get_sigverify(&self) -> bool {
self.version.sigverify
}
#[cfg(feature = "internal-test")]
pub fn get_feature_set(&self) -> Arc<FeatureSet> {
Arc::clone(&self.version.feature_set)
}
pub(crate) 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() })
}
pub(crate) fn check_transaction_age_inner(
&self,
tx: &SanitizedTransaction,
) -> solana_transaction_error::TransactionResult<()> {
let recent_blockhash = tx.message().recent_blockhash();
if recent_blockhash == &self.block_env.latest_blockhash ||
self.check_transaction_for_nonce(
tx,
&DurableNonce::from_blockhash(&self.block_env.latest_blockhash),
)
{
Ok(())
} else {
tracing::error!(
"Blockhash {} not found. Expected blockhash {}",
recent_blockhash,
self.block_env.latest_blockhash
);
Err(TransactionError::BlockhashNotFound)
}
}
pub(crate) fn check_message_for_nonce(&self, message: &SanitizedMessage) -> bool {
SVMMessage::get_durable_nonce(message)
.and_then(|nonce_address| self.accounts.get_account(nonce_address))
.and_then(|nonce_account| {
verify_nonce_account(&nonce_account, message.recent_blockhash())
})
.is_some_and(|nonce_data| {
SVMStaticMessage::get_ix_signers(message, NONCED_TX_MARKER_IX_INDEX as usize)
.any(|signer| signer == &nonce_data.authority)
})
}
pub(crate) 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<T> + 'static>(
&mut self,
callback: C,
) {
self.invocation_inspect_callback = Arc::new(callback);
}
pub fn register_custom_syscall(
&mut self,
name: &str,
syscall: BuiltinFunctionRegisterer,
) -> Result<(), HPSVMError> {
self.runtime_registry.register_custom_syscall(CustomSyscallRegistration {
name: name.to_owned(),
function: syscall,
});
self.try_refresh_runtime_environments()?;
self.sync_jit_policy();
self.accounts.rebuild_program_cache().map_err(HPSVMError::from)?;
Ok(())
}
}
impl<T: SvmTypesHost> Svm<'_, T> {
pub(crate) fn instruction_chain_transaction(
&self,
instructions: &[Instruction],
) -> VersionedTransaction {
let fee_payer = fee_payer_for_instructions(instructions, self.airdrop_pubkey());
let message = Message::new_with_blockhash(
instructions,
Some(&fee_payer),
&self.block_env.latest_blockhash,
);
let signatures =
vec![Signature::default(); usize::from(message.header.num_required_signatures)];
VersionedTransaction { signatures, message: VersionedMessage::Legacy(message) }
}
}
fn execution_into_detached<T: SvmTypesHost>(
vm: &Svm<'_, T>,
execution: ExecutionResult,
log_collector: Rc<RefCell<LogCollector>>,
method_name: &str,
) -> DetachedTx {
let ExecutionResult {
post_accounts,
tx_result,
signature,
compute_units_consumed,
inner_instructions,
return_data,
execution_trace,
included,
fee,
fee_payer,
account_source_failures,
fatal_error,
inspector_live,
} = execution;
let Ok(logs) = Rc::try_unwrap(log_collector).map(|collector| collector.into_inner().messages)
else {
unreachable!("Log collector should not be used after {method_name} returns")
};
DetachedTx {
meta: TransactionMetadata {
signature,
logs,
inner_instructions,
compute_units_consumed,
return_data,
fee,
diagnostics: if vm.version.compute_diagnostics {
hotpath_block!("hpsvm::execution_into_detached::diagnostics", {
execution_diagnostics(
vm,
&post_accounts,
execution_trace,
account_source_failures,
)
})
} else {
ExecutionDiagnostics {
execution_trace,
account_source_failures,
..Default::default()
}
},
},
post_accounts,
status: tx_result,
included,
fee_payer,
fatal_error,
inspector_live,
}
}
fn execution_diagnostics<T: SvmTypesHost>(
vm: &Svm<'_, T>,
post_accounts: &[(Address, AccountSharedData)],
execution_trace: ExecutionTrace,
account_source_failures: Vec<AccountSourceFailure>,
) -> ExecutionDiagnostics {
let mut pre_balances = Vec::with_capacity(post_accounts.len());
let mut post_balances = Vec::with_capacity(post_accounts.len());
let mut account_diffs = Vec::new();
let mut pre_accounts = Vec::with_capacity(post_accounts.len());
for (address, post) in post_accounts {
let pre = vm.accounts.get_account(address).unwrap_or_default();
pre_balances.push(pre.lamports());
post_balances.push(post.lamports());
if accounts_differ(&pre, post) {
account_diffs.push(AccountDiff {
address: *address,
pre: public_account_from_shared(&pre),
post: public_account_from_shared(post),
});
}
pre_accounts.push((*address, pre));
}
ExecutionDiagnostics {
pre_balances,
post_balances,
account_diffs,
account_source_failures,
pre_token_balances: hotpath_block!("hpsvm::diagnostics::pre_token_balances", {
token_balances(&pre_accounts, &vm.accounts)
}),
post_token_balances: hotpath_block!("hpsvm::diagnostics::post_token_balances", {
token_balances(post_accounts, &vm.accounts)
}),
execution_trace,
}
}
fn fee_payer_for_instruction_case(case: &InstructionCase) -> Address {
case.accounts
.iter()
.find(|account| account.is_signer)
.or_else(|| case.accounts.iter().find(|account| account.is_writable))
.or_else(|| case.accounts.first())
.map(|account| account.pubkey)
.unwrap_or_else(Address::new_unique)
}
#[cfg(test)]
mod tests {
use solana_instruction::{Instruction, account_meta::AccountMeta};
use solana_message::{Message, VersionedMessage};
use solana_signer::Signer;
use solana_system_interface::{instruction::transfer, program as system_program};
use solana_transaction::{InstructionError, Transaction};
use super::*;
#[test]
pub(crate) fn sysvar_accounts_are_demoted_to_readonly() {
let payer = Keypair::new();
let svm = Hpsvm::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(tx).unwrap();
assert!(!sanitized.message().is_writable(1));
}
#[test]
pub(crate) fn with_transaction_origin_restores_previous_origin_after_panic() {
let mut svm = Hpsvm::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
svm.with_transaction_origin(TransactionOrigin::InternalAirdrop, |_| {
panic!("boom");
});
}));
assert!(result.is_err());
assert_eq!(svm.inspection_origin, TransactionOrigin::User);
}
#[test]
pub(crate) fn set_feature_set_rolls_back_failed_reconfiguration() {
let mut svm = Hpsvm::new();
let payer = Keypair::new();
let recipient = Address::new_unique();
let original_feature_set = Arc::clone(&svm.version.feature_set);
let original_reserved_account_keys = svm.reserved_account_keys.clone();
svm.airdrop(&payer.pubkey(), 10_000).unwrap();
let poisoned_program = Address::new_unique();
let mut invalid_program = AccountSharedData::new(1, 0, &system_program::id());
invalid_program.set_executable(true);
svm.accounts.add_account_no_checks(poisoned_program, invalid_program);
let err = svm
.set_feature_set(FeatureSet::all_enabled())
.expect_err("invalid cached program should abort feature-set reconfiguration");
assert!(matches!(err, HPSVMError::Instruction(InstructionError::IncorrectProgramId)));
assert_eq!(svm.version.feature_set, original_feature_set);
assert_eq!(svm.reserved_account_keys.active, original_reserved_account_keys.active);
let tx = Transaction::new(
&[&payer],
Message::new(&[transfer(&payer.pubkey(), &recipient, 64)], Some(&payer.pubkey())),
svm.latest_blockhash(),
);
let result = svm.transact(tx).expect("transact should stage").commit();
assert!(result.is_ok());
assert_eq!(svm.get_balance(&recipient), Some(64));
}
}
#[cfg(test)]
mod jit_parity_tests {
use std::sync::Arc;
use agave_feature_set::FeatureSet;
use solana_address::Address;
use solana_instruction::Instruction;
use solana_keypair::Keypair;
use solana_message::{Message, VersionedMessage};
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use super::*;
use crate::{JitPolicy, ProgramTierStats};
fn memo_elf_v3() -> &'static [u8] {
include_bytes!("../elf/spl_memo-3.0.0.so")
}
fn memo_elf_v1() -> &'static [u8] {
include_bytes!("../elf/spl_memo-1.0.0.so")
}
fn fresh_non_default_vm() -> Hpsvm {
Hpsvm::builder()
.with_feature_set(FeatureSet::default())
.with_builtins()
.with_sysvars()
.with_lamports(1_000_000_000)
.with_sigverify(true)
.with_blockhash_check(true)
.build()
.expect("non-default VM should build")
}
fn memo_instruction(program_id: Address) -> Instruction {
Instruction { program_id, accounts: vec![], data: b"hello jit".to_vec() }
}
fn memo_transaction(svm: &Hpsvm, payer: &Keypair, program_id: Address) -> VersionedTransaction {
let ix = memo_instruction(program_id);
let msg =
Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &svm.latest_blockhash());
VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer])
.expect("tx should build")
}
#[test]
fn default_vms_share_default_program_executables() {
let a = Hpsvm::new();
let b = Hpsvm::new();
let memo_id = solana_address::address!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
let a_entry = a
.accounts
.shared_programs_cache()
.read()
.find(&memo_id)
.expect("default VM loads memo");
let b_entry =
b.accounts.shared_programs_cache().read().find(&memo_id).expect("second VM loads memo");
assert!(Arc::ptr_eq(&a_entry, &b_entry), "default VMs must share the memoized executable");
}
#[test]
fn every_constructor_yields_eager_and_builder_round_trips() {
assert_eq!(Hpsvm::new().version.jit_policy, JitPolicy::Eager);
assert_eq!(
Hpsvm::builder()
.with_program_test_defaults()
.build()
.expect("build")
.version
.jit_policy,
JitPolicy::Eager
);
let lazy = JitPolicy::Lazy { hot_threshold: 8 };
let vm = Hpsvm::builder()
.with_program_test_defaults()
.with_jit_policy(lazy)
.build()
.expect("build with policy");
assert_eq!(vm.version.jit_policy, lazy);
let off_vm = Hpsvm::new().with_jit_policy(JitPolicy::Off);
assert_eq!(off_vm.version.jit_policy, JitPolicy::Off);
let mut mutable = Hpsvm::new();
mutable.set_jit_policy(lazy);
assert_eq!(mutable.version.jit_policy, lazy);
}
#[test]
fn uncompiled_load_is_structurally_equal_modulo_compiled() {
use solana_program_runtime::{
loaded_programs::{ProgramRuntimeEnvironment, ProgramRuntimeEnvironments},
program_cache_entry::ProgramCacheEntry,
program_metrics::LoadProgramMetrics,
solana_sbpf::program::BuiltinProgram,
};
let loader = solana_sdk_ids::bpf_loader::id();
let env_inner =
ProgramRuntimeEnvironment::from(BuiltinProgram::new_loader(Default::default()));
let envs = ProgramRuntimeEnvironments::new(env_inner.clone(), env_inner.clone());
let env = envs.get_env_for_execution();
let elf = memo_elf_v3();
let eager = ProgramCacheEntry::load(
&loader,
env.clone(),
crate::accounts_db::IMMEDIATE_DEPLOYMENT_SLOT,
elf,
&mut LoadProgramMetrics::default(),
)
.expect("eager load should succeed");
let uncompiled = crate::programs::load_program_uncompiled(&loader, env, elf)
.expect("uncompiled load should succeed");
assert_eq!(eager, uncompiled);
#[cfg(target_arch = "x86_64")]
{
assert!(
crate::programs::has_compiled_program(&eager),
"eager entry must be compiled on x86_64"
);
assert!(
!crate::programs::has_compiled_program(&uncompiled),
"uncompiled entry must report no compiled program"
);
}
#[cfg(not(target_arch = "x86_64"))]
{
assert!(!crate::programs::has_compiled_program(&eager));
assert!(!crate::programs::has_compiled_program(&uncompiled));
}
}
#[test]
fn uncompiled_entry_executes_successfully_interpreted() {
if !cfg!(target_arch = "x86_64") {
return;
}
let mut svm = fresh_non_default_vm().with_jit_policy(JitPolicy::Off);
let program_id = Address::new_unique();
svm.add_program_with_loader(program_id, memo_elf_v3(), solana_sdk_ids::bpf_loader::id())
.expect("add should succeed");
let entry = svm
.accounts
.shared_programs_cache()
.read()
.find(&program_id)
.expect("entry must exist");
assert!(!crate::programs::has_compiled_program(&entry));
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
let tx = memo_transaction(&svm, &payer, program_id);
let result = svm.send_transaction(tx);
assert!(result.is_ok(), "interpreted execution should succeed: {result:?}");
}
#[test]
fn resident_store_hits_misses_and_env_scoping() {
let mut svm = fresh_non_default_vm();
let elf_a = memo_elf_v3();
let elf_b = memo_elf_v1();
assert_ne!(elf_a, elf_b, "vendored memo ELFs must differ for the miss test");
let id_a1 = Address::new_unique();
let id_a2 = Address::new_unique();
svm.add_program_with_loader(id_a1, elf_a, solana_sdk_ids::bpf_loader::id())
.expect("add a1");
let entry_a1 = svm.accounts.shared_programs_cache().read().find(&id_a1).expect("a1 entry");
svm.add_program_with_loader(id_a2, elf_a, solana_sdk_ids::bpf_loader::id())
.expect("add a2 same bytes");
let entry_a2 = svm.accounts.shared_programs_cache().read().find(&id_a2).expect("a2 entry");
assert!(
Arc::ptr_eq(&entry_a1, &entry_a2),
"identical bytes under one env must share the resident entry"
);
let id_b = Address::new_unique();
svm.add_program_with_loader(id_b, elf_b, solana_sdk_ids::bpf_loader_deprecated::id())
.expect("add b");
let entry_b = svm.accounts.shared_programs_cache().read().find(&id_b).expect("b entry");
assert!(!Arc::ptr_eq(&entry_a1, &entry_b), "different bytes must miss");
let mut other = fresh_non_default_vm();
let id_other = Address::new_unique();
other
.add_program_with_loader(id_other, elf_a, solana_sdk_ids::bpf_loader::id())
.expect("add other env");
let entry_other =
other.accounts.shared_programs_cache().read().find(&id_other).expect("other entry");
assert!(
!Arc::ptr_eq(&entry_a1, &entry_other),
"entries must not cross environment identities"
);
}
#[test]
fn rebuild_relinks_unchanged_programs() {
let mut svm = fresh_non_default_vm();
let elf_a = memo_elf_v3();
let elf_b = memo_elf_v1();
let id_a = Address::new_unique();
let id_b = Address::new_unique();
let loader = solana_sdk_ids::bpf_loader::id();
svm.add_program_with_loader(id_a, elf_a, loader).expect("add a");
svm.add_program_with_loader(id_b, elf_a, loader).expect("add b same bytes");
let before_a = svm.accounts.shared_programs_cache().read().find(&id_a).expect("a before");
let before_b = svm.accounts.shared_programs_cache().read().find(&id_b).expect("b before");
assert!(Arc::ptr_eq(&before_a, &before_b));
{
use solana_account::AccountSharedData;
let lamports = svm.minimum_balance_for_rent_exemption(elf_b.len());
let mut upgraded = AccountSharedData::new(lamports, elf_b.len(), &loader);
upgraded.set_executable(true);
upgraded.set_data_from_slice(elf_b);
svm.accounts.add_account_no_checks(id_b, upgraded);
}
svm.sync_jit_policy();
svm.accounts.rebuild_program_cache().expect("rebuild should succeed");
let after_a = svm.accounts.shared_programs_cache().read().find(&id_a).expect("a after");
let after_b = svm.accounts.shared_programs_cache().read().find(&id_b).expect("b after");
assert!(
Arc::ptr_eq(&before_a, &after_a),
"unchanged program must re-link the resident entry"
);
assert!(!Arc::ptr_eq(&before_b, &after_b), "upgraded program must resolve a fresh entry");
}
#[test]
fn lazy_zero_promotes_synchronously_and_stats_reflect_it() {
if !cfg!(target_arch = "x86_64") {
let svm = fresh_non_default_vm().with_jit_policy(JitPolicy::Lazy { hot_threshold: 0 });
let stats = svm.program_tier_stats();
assert_eq!(stats.effective_policy, JitPolicy::Off);
assert_eq!(stats.promotions_attempted, 0);
return;
}
let mut svm = fresh_non_default_vm().with_jit_policy(JitPolicy::Lazy { hot_threshold: 0 });
let program_id = Address::new_unique();
svm.add_program_with_loader(program_id, memo_elf_v3(), solana_sdk_ids::bpf_loader::id())
.expect("add");
let before = svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry");
assert!(!crate::programs::has_compiled_program(&before));
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
let tx = memo_transaction(&svm, &payer, program_id);
let result = svm.send_transaction(tx);
assert!(result.is_ok(), "{result:?}");
let after =
svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry after");
assert!(
crate::programs::has_compiled_program(&after),
"threshold-0 must promote after first use"
);
let stats = svm.program_tier_stats();
assert_eq!(stats.policy, JitPolicy::Lazy { hot_threshold: 0 });
assert_eq!(stats.effective_policy, JitPolicy::Lazy { hot_threshold: 0 });
assert!(stats.promotions_attempted >= 1);
assert!(stats.promotions_succeeded >= 1);
assert!(stats.cache_entries >= 1);
assert!(stats.resident_entries >= 1);
}
#[test]
fn promotion_failure_leaves_entry_interpreted_and_tx_succeeds() {
if !cfg!(target_arch = "x86_64") {
return;
}
let mut svm = fresh_non_default_vm().with_jit_policy(JitPolicy::Lazy { hot_threshold: 0 });
let program_id = Address::new_unique();
svm.add_program_with_loader(program_id, memo_elf_v3(), solana_sdk_ids::bpf_loader::id())
.expect("add");
svm.tier_promoter.set_inject_failure(true);
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
let tx = memo_transaction(&svm, &payer, program_id);
let result = svm.send_transaction(tx);
assert!(result.is_ok(), "promotion failure must not fail the transaction: {result:?}");
let entry = svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry");
assert!(
!crate::programs::has_compiled_program(&entry),
"failed promotion must leave the entry interpreted"
);
let stats = svm.program_tier_stats();
assert!(stats.promotions_failed >= 1);
}
#[test]
fn eager_and_off_never_promote() {
let mut eager = fresh_non_default_vm().with_jit_policy(JitPolicy::Eager);
let mut off = fresh_non_default_vm().with_jit_policy(JitPolicy::Off);
let elf = memo_elf_v3();
let loader = solana_sdk_ids::bpf_loader::id();
let id_e = Address::new_unique();
let id_o = Address::new_unique();
eager.add_program_with_loader(id_e, elf, loader).expect("add eager");
off.add_program_with_loader(id_o, elf, loader).expect("add off");
if cfg!(target_arch = "x86_64") {
let payer = Keypair::new();
off.airdrop(&payer.pubkey(), 10_000_000).unwrap();
let tx = memo_transaction(&off, &payer, id_o);
let result = off.send_transaction(tx);
assert!(result.is_ok());
let entry = off.accounts.shared_programs_cache().read().find(&id_o).expect("off entry");
assert!(!crate::programs::has_compiled_program(&entry));
}
assert_eq!(eager.program_tier_stats().promotions_attempted, 0);
assert_eq!(off.program_tier_stats().promotions_attempted, 0);
eager.observe_tier();
off.observe_tier();
assert_eq!(eager.program_tier_stats().promotions_attempted, 0);
assert_eq!(off.program_tier_stats().promotions_attempted, 0);
}
#[test]
fn prewarm_resolves_and_promotes_under_lazy_zero() {
if !cfg!(target_arch = "x86_64") {
return;
}
let mut svm = fresh_non_default_vm().with_jit_policy(JitPolicy::Lazy { hot_threshold: 0 });
let program_id = Address::new_unique();
svm.add_program_with_loader(program_id, memo_elf_v3(), solana_sdk_ids::bpf_loader::id())
.expect("add");
let before = svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry");
assert!(!crate::programs::has_compiled_program(&before));
svm.prewarm_programs(&[program_id]).expect("prewarm should succeed");
let after = svm
.accounts
.shared_programs_cache()
.read()
.find(&program_id)
.expect("entry after prewarm");
assert!(crate::programs::has_compiled_program(&after), "Lazy-0 prewarm must promote");
}
#[test]
fn tier_stats_are_zeroed_off_target() {
let svm = Hpsvm::new();
let stats: ProgramTierStats = svm.program_tier_stats();
assert_eq!(stats.policy, JitPolicy::Eager);
if cfg!(target_arch = "x86_64") {
assert_eq!(stats.effective_policy, JitPolicy::Eager);
} else {
assert_eq!(stats.effective_policy, JitPolicy::Off);
assert_eq!(stats.promotions_attempted, 0);
}
}
#[test]
fn jit_policy_with_round_trips_through_version() {
use crate::ClusterSpecId;
let base = crate::Version::base(ClusterSpecId::MainnetBeta).clone();
assert_eq!(base.jit_policy, JitPolicy::Eager);
let lazy = base.with_jit_policy(JitPolicy::Lazy { hot_threshold: 3 });
assert_eq!(lazy.jit_policy, JitPolicy::Lazy { hot_threshold: 3 });
}
}
#[cfg(test)]
mod jit_threshold_boundary_tests {
use std::time::{Duration, Instant};
use solana_address::Address;
use solana_keypair::Keypair;
use solana_message::{Message, VersionedMessage};
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use super::*;
use crate::JitPolicy;
fn memo_elf() -> &'static [u8] {
include_bytes!("../elf/spl_memo-3.0.0.so")
}
#[test]
fn hot_threshold_boundary_promotes_exactly_at_threshold() {
if !cfg!(target_arch = "x86_64") {
return;
}
let mut svm = Hpsvm::builder()
.with_feature_set(agave_feature_set::FeatureSet::default())
.with_builtins()
.with_sysvars()
.with_lamports(1_000_000_000)
.with_sigverify(true)
.with_blockhash_check(true)
.build()
.expect("VM should build")
.with_jit_policy(JitPolicy::Lazy { hot_threshold: 2 });
svm.set_transaction_history(0);
let program_id = Address::new_unique();
svm.add_program_with_loader(program_id, memo_elf(), solana_sdk_ids::bpf_loader::id())
.expect("add should succeed");
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
let tx_for = |svm: &Hpsvm, nonce: u8| {
let mut data = b"hello jit".to_vec();
data.push(nonce);
let ix = solana_instruction::Instruction { program_id, accounts: vec![], data };
let msg =
Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &svm.latest_blockhash());
VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer])
.expect("tx should build")
};
let result = svm.send_transaction(tx_for(&svm, 0));
assert!(result.is_ok(), "{result:?}");
std::thread::sleep(Duration::from_millis(50));
let after_first =
svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry");
assert!(
!crate::programs::has_compiled_program(&after_first),
"below threshold must stay interpreted"
);
let result = svm.send_transaction(tx_for(&svm, 1));
assert!(result.is_ok(), "{result:?}");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let entry =
svm.accounts.shared_programs_cache().read().find(&program_id).expect("entry");
if crate::programs::has_compiled_program(&entry) {
break;
}
if Instant::now() > deadline {
panic!("threshold promotion did not publish within 10s");
}
std::thread::sleep(Duration::from_millis(10));
}
let stats = svm.program_tier_stats();
assert!(stats.promotions_attempted >= 1);
assert!(stats.promotions_succeeded >= 1);
}
}