From de9846156919fe8eb11fc8798c50b31b4e3dbcdd Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Tue, 28 Oct 2025 00:53:14 +0000 Subject: [PATCH 1/2] harness: generic-vm: add marker trait --- harness/src/fuzz/firedancer.rs | 5 +- harness/src/fuzz/mod.rs | 12 ++-- harness/src/fuzz/mollusk.rs | 6 +- harness/src/lib.rs | 96 ++++++++++++++++++++++++++++---- harness/src/vm.rs | 13 +++++ harness/tests/fd_test_vectors.rs | 11 ++-- 6 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 harness/src/vm.rs diff --git a/harness/src/fuzz/firedancer.rs b/harness/src/fuzz/firedancer.rs index 0013538e..2a8e2c4a 100644 --- a/harness/src/fuzz/firedancer.rs +++ b/harness/src/fuzz/firedancer.rs @@ -7,6 +7,7 @@ use { crate::{ compile_accounts::{compile_accounts, CompiledAccounts}, + vm::SolanaVM, Mollusk, DEFAULT_LOADER_KEY, }, agave_feature_set::FeatureSet, @@ -235,8 +236,8 @@ fn instruction_metadata() -> FuzzMetadata { } } -pub fn build_fixture_from_mollusk_test( - mollusk: &Mollusk, +pub fn build_fixture_from_mollusk_test( + mollusk: &Mollusk, instruction: &Instruction, accounts: &[(Pubkey, Account)], result: &InstructionResult, diff --git a/harness/src/fuzz/mod.rs b/harness/src/fuzz/mod.rs index d71cc8fa..0e7f0861 100644 --- a/harness/src/fuzz/mod.rs +++ b/harness/src/fuzz/mod.rs @@ -4,12 +4,16 @@ pub mod firedancer; pub mod mollusk; use { - crate::Mollusk, mollusk_svm_fuzz_fs::FsHandler, mollusk_svm_result::InstructionResult, - solana_account::Account, solana_instruction::Instruction, solana_pubkey::Pubkey, + crate::{vm::SolanaVM, Mollusk}, + mollusk_svm_fuzz_fs::FsHandler, + mollusk_svm_result::InstructionResult, + solana_account::Account, + solana_instruction::Instruction, + solana_pubkey::Pubkey, }; -pub fn generate_fixtures_from_mollusk_test( - mollusk: &Mollusk, +pub fn generate_fixtures_from_mollusk_test( + mollusk: &Mollusk, instruction: &Instruction, accounts: &[(Pubkey, Account)], result: &InstructionResult, diff --git a/harness/src/fuzz/mollusk.rs b/harness/src/fuzz/mollusk.rs index f9a20fc3..c64622f9 100644 --- a/harness/src/fuzz/mollusk.rs +++ b/harness/src/fuzz/mollusk.rs @@ -5,7 +5,7 @@ //! Only available when the `fuzz` feature is enabled. use { - crate::{sysvar::Sysvars, Mollusk}, + crate::{sysvar::Sysvars, vm::SolanaVM, Mollusk}, agave_feature_set::FeatureSet, mollusk_svm_fuzz_fixture::{ context::Context as FuzzContext, effects::Effects as FuzzEffects, @@ -102,8 +102,8 @@ pub(crate) fn parse_fixture_context(context: &FuzzContext) -> ParsedFixtureConte } } -pub fn build_fixture_from_mollusk_test( - mollusk: &Mollusk, +pub fn build_fixture_from_mollusk_test( + mollusk: &Mollusk, instruction: &Instruction, accounts: &[(Pubkey, Account)], result: &InstructionResult, diff --git a/harness/src/lib.rs b/harness/src/lib.rs index b55aa8fb..d267f425 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -447,6 +447,7 @@ pub mod file; pub mod fuzz; pub mod program; pub mod sysvar; +pub mod vm; // Re-export result module from mollusk-svm-result crate pub use mollusk_svm_result as result; @@ -458,8 +459,12 @@ use solana_precompile_error::PrecompileError; use solana_transaction_context::InstructionAccount; use { crate::{ - account_store::AccountStore, compile_accounts::CompiledAccounts, epoch_stake::EpochStake, - program::ProgramCache, sysvar::Sysvars, + account_store::AccountStore, + compile_accounts::CompiledAccounts, + epoch_stake::EpochStake, + program::ProgramCache, + sysvar::Sysvars, + vm::{agave::AgaveVM, SolanaVM}, }, agave_feature_set::FeatureSet, mollusk_svm_error::error::{MolluskError, MolluskPanic}, @@ -474,7 +479,7 @@ use { solana_svm_log_collector::LogCollector, solana_svm_timings::ExecuteTimings, solana_transaction_context::TransactionContext, - std::{cell::RefCell, collections::HashSet, iter::once, rc::Rc}, + std::{cell::RefCell, collections::HashSet, iter::once, marker::PhantomData, rc::Rc}, }; pub(crate) const DEFAULT_LOADER_KEY: Pubkey = solana_sdk_ids::bpf_loader_upgradeable::id(); @@ -483,7 +488,7 @@ pub(crate) const DEFAULT_LOADER_KEY: Pubkey = solana_sdk_ids::bpf_loader_upgrade /// /// All fields can be manipulated through a handful of helper methods, but /// users can also directly access and modify them if they desire more control. -pub struct Mollusk { +pub struct Mollusk { pub config: Config, pub compute_budget: ComputeBudget, pub epoch_stake: EpochStake, @@ -492,6 +497,8 @@ pub struct Mollusk { pub program_cache: ProgramCache, pub sysvars: Sysvars, + vm: PhantomData, + /// The callback which can be used to inspect invoke_context /// and extract low-level information such as bpf traces, transaction /// context, detailed timings, etc. @@ -531,7 +538,24 @@ impl InvocationInspectCallback for EmptyInvocationInspectCallback { fn after_invocation(&self, _: &InvokeContext) {} } -impl Default for Mollusk { +// Fields from `Mollusk`, minus the generic VM. +struct MolluskFields { + config: Config, + compute_budget: ComputeBudget, + epoch_stake: EpochStake, + feature_set: FeatureSet, + logger: Option>>, + program_cache: ProgramCache, + sysvars: Sysvars, + + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback: Box, + + #[cfg(feature = "fuzz-fd")] + slot: u64, +} + +impl Default for MolluskFields { fn default() -> Self { #[rustfmt::skip] solana_logger::setup_with_default( @@ -572,7 +596,35 @@ impl Default for Mollusk { } } -impl CheckContext for Mollusk { +impl MolluskFields { + fn vend(self) -> Mollusk { + Mollusk { + config: self.config, + compute_budget: self.compute_budget, + epoch_stake: self.epoch_stake, + feature_set: self.feature_set, + logger: self.logger, + program_cache: self.program_cache, + sysvars: self.sysvars, + + vm: PhantomData, + + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback: self.invocation_inspect_callback, + + #[cfg(feature = "fuzz-fd")] + slot: self.slot, + } + } +} + +impl Default for Mollusk { + fn default() -> Self { + MolluskFields::default().vend() + } +} + +impl CheckContext for Mollusk { fn is_rent_exempt(&self, lamports: u64, space: usize, owner: Pubkey) -> bool { owner.eq(&Pubkey::default()) && lamports == 0 || self.sysvars.rent.is_exempt(lamports, space) @@ -649,7 +701,29 @@ impl Mollusk { /// - The directory specified by the `SBF_OUT_DIR` environment variable /// - The current working directory pub fn new(program_id: &Pubkey, program_name: &str) -> Self { - let mut mollusk = Self::default(); + let mut mollusk = MolluskFields::default().vend(); + mollusk.add_program(program_id, program_name, &DEFAULT_LOADER_KEY); + mollusk + } +} + +impl Mollusk { + /// Create a new Mollusk instance with a custom VM type. + /// + /// Attempts to load the program's ELF file from the default search paths. + /// Once loaded, adds the program to the program cache and returns the + /// newly created Mollusk instance. + /// + /// # Default Search Paths + /// + /// The following locations are checked in order: + /// + /// - `tests/fixtures` + /// - The directory specified by the `BPF_OUT_DIR` environment variable + /// - The directory specified by the `SBF_OUT_DIR` environment variable + /// - The current working directory + pub fn new_with_vm(program_id: &Pubkey, program_name: &str) -> Self { + let mut mollusk = MolluskFields::default().vend(); mollusk.add_program(program_id, program_name, &DEFAULT_LOADER_KEY); mollusk } @@ -1163,7 +1237,7 @@ impl Mollusk { /// instruction executions, starting with the provided account store. /// /// See [`MolluskContext`] for more details on how to use it. - pub fn with_context(self, mut account_store: AS) -> MolluskContext { + pub fn with_context(self, mut account_store: AS) -> MolluskContext { // For convenience, load all program accounts into the account store, // but only if they don't exist. self.program_cache @@ -1199,13 +1273,13 @@ impl Mollusk { /// management and a streamlined interface. Namely, the input `accounts` slice /// is no longer required, and the returned result does not contain a /// `resulting_accounts` field. -pub struct MolluskContext { - pub mollusk: Mollusk, +pub struct MolluskContext { + pub mollusk: Mollusk, pub account_store: Rc>, pub hydrate_store: bool, } -impl MolluskContext { +impl MolluskContext { fn load_accounts_for_instructions<'a>( &self, instructions: impl Iterator, diff --git a/harness/src/vm.rs b/harness/src/vm.rs new file mode 100644 index 00000000..d88baa1e --- /dev/null +++ b/harness/src/vm.rs @@ -0,0 +1,13 @@ +//! Virtual Machine API for using Mollusk with custom VMs. + +/// A virtual machine compatible with the Solana calling convention. +pub trait SolanaVM {} + +pub mod agave { + use super::SolanaVM; + + /// The SBPF virtual machine used in Anza's Agave validator. + pub struct AgaveVM {} + + impl SolanaVM for AgaveVM {} +} diff --git a/harness/tests/fd_test_vectors.rs b/harness/tests/fd_test_vectors.rs index 8da7cc1d..eaf9082e 100644 --- a/harness/tests/fd_test_vectors.rs +++ b/harness/tests/fd_test_vectors.rs @@ -62,11 +62,12 @@ fn test_load_firedancer_fixtures() { }, result, ) = load_firedancer_fixture(&loaded_fixture); - let mollusk = Mollusk { - compute_budget, - feature_set, - slot, - ..Default::default() + let mollusk = { + let mut mollusk = Mollusk::default(); + mollusk.compute_budget = compute_budget; + mollusk.feature_set = feature_set; + mollusk.slot = slot; + mollusk }; let generated_fixture = build_fixture_from_mollusk_test(&mollusk, &instruction, &accounts, &result); From d5737692651c07c77d10a11144867d449c245d35 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Tue, 28 Oct 2025 05:12:32 +0000 Subject: [PATCH 2/2] harness: generic-vm: agave vm v1 --- harness/src/lib.rs | 72 +++++++++++++++++------------------------ harness/src/vm.rs | 13 -------- harness/src/vm/agave.rs | 69 +++++++++++++++++++++++++++++++++++++++ harness/src/vm/mod.rs | 51 +++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 55 deletions(-) delete mode 100644 harness/src/vm.rs create mode 100644 harness/src/vm/agave.rs create mode 100644 harness/src/vm/mod.rs diff --git a/harness/src/lib.rs b/harness/src/lib.rs index d267f425..2ade4957 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -455,8 +455,6 @@ pub use mollusk_svm_result as result; use mollusk_svm_result::Compare; #[cfg(feature = "precompiles")] use solana_precompile_error::PrecompileError; -#[cfg(feature = "invocation-inspect-callback")] -use solana_transaction_context::InstructionAccount; use { crate::{ account_store::AccountStore, @@ -464,7 +462,7 @@ use { epoch_stake::EpochStake, program::ProgramCache, sysvar::Sysvars, - vm::{agave::AgaveVM, SolanaVM}, + vm::{agave::AgaveVM, SolanaVM, SolanaVMContext, SolanaVMInstruction, SolanaVMTrace}, }, agave_feature_set::FeatureSet, mollusk_svm_error::error::{MolluskError, MolluskPanic}, @@ -473,7 +471,7 @@ use { solana_compute_budget::compute_budget::ComputeBudget, solana_hash::Hash, solana_instruction::{AccountMeta, Instruction}, - solana_program_runtime::invoke_context::{EnvironmentConfig, InvokeContext}, + solana_program_runtime::invoke_context::EnvironmentConfig, solana_pubkey::Pubkey, solana_svm_callback::InvokeContextCallback, solana_svm_log_collector::LogCollector, @@ -481,6 +479,11 @@ use { solana_transaction_context::TransactionContext, std::{cell::RefCell, collections::HashSet, iter::once, marker::PhantomData, rc::Rc}, }; +#[cfg(feature = "invocation-inspect-callback")] +use { + solana_program_runtime::invoke_context::InvokeContext, + solana_transaction_context::InstructionAccount, +}; pub(crate) const DEFAULT_LOADER_KEY: Pubkey = solana_sdk_ids::bpf_loader_upgradeable::id(); @@ -795,54 +798,39 @@ impl Mollusk { }; let runtime_features = self.feature_set.runtime_features(); let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts); - let mut invoke_context = InvokeContext::new( - &mut transaction_context, - &mut program_cache, - EnvironmentConfig::new( + + let context = SolanaVMContext { + transaction_context: &mut transaction_context, + program_cache: &mut program_cache, + compute_budget: self.compute_budget, + environment_config: EnvironmentConfig::new( Hash::default(), /* blockhash_lamports_per_signature */ 5000, // The default value &callback, &runtime_features, &sysvar_cache, ), - self.logger.clone(), - self.compute_budget.to_budget(), - self.compute_budget.to_cost(), - ); - - // Configure the next instruction frame for this invocation. - invoke_context - .transaction_context - .configure_next_instruction_for_tests( - program_id_index, - instruction_accounts.clone(), - &instruction.data, - ) - .expect("failed to configure next instruction"); - - #[cfg(feature = "invocation-inspect-callback")] - self.invocation_inspect_callback.before_invocation( - &instruction.program_id, - &instruction.data, - &instruction_accounts, - &invoke_context, - ); + }; - let result = if invoke_context.is_precompile(&instruction.program_id) { - invoke_context.process_precompile( - &instruction.program_id, - &instruction.data, - std::iter::once(instruction.data.as_ref()), - ) - } else { - invoke_context.process_instruction(&mut compute_units_consumed, &mut timings) + let instruction = SolanaVMInstruction { + program_id_index, + accounts: instruction_accounts, + data: &instruction.data, }; - #[cfg(feature = "invocation-inspect-callback")] - self.invocation_inspect_callback - .after_invocation(&invoke_context); + let trace = SolanaVMTrace { + compute_units_consumed: &mut compute_units_consumed, + execute_timings: &mut timings, + log_collector: self.logger.clone(), + }; - result + VM::process_instruction( + context, + instruction, + trace, + #[cfg(feature = "invocation-inspect-callback")] + self.invocation_inspect_callback.as_ref(), + ) }; let return_data = transaction_context.get_return_data().1.to_vec(); diff --git a/harness/src/vm.rs b/harness/src/vm.rs deleted file mode 100644 index d88baa1e..00000000 --- a/harness/src/vm.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Virtual Machine API for using Mollusk with custom VMs. - -/// A virtual machine compatible with the Solana calling convention. -pub trait SolanaVM {} - -pub mod agave { - use super::SolanaVM; - - /// The SBPF virtual machine used in Anza's Agave validator. - pub struct AgaveVM {} - - impl SolanaVM for AgaveVM {} -} diff --git a/harness/src/vm/agave.rs b/harness/src/vm/agave.rs new file mode 100644 index 00000000..aa6cc3b7 --- /dev/null +++ b/harness/src/vm/agave.rs @@ -0,0 +1,69 @@ +//! The SBPF virtual machine used in Anza's Agave validator. + +#[cfg(feature = "invocation-inspect-callback")] +use crate::InvocationInspectCallback; +use { + super::{SolanaVM, SolanaVMContext, SolanaVMInstruction, SolanaVMTrace}, + solana_instruction_error::InstructionError, + solana_program_runtime::invoke_context::InvokeContext, +}; + +/// The SBPF virtual machine used in Anza's Agave validator. +pub struct AgaveVM {} + +impl SolanaVM for AgaveVM { + fn process_instruction( + context: SolanaVMContext, + instruction: SolanaVMInstruction, + trace: SolanaVMTrace, + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback: &dyn InvocationInspectCallback, + ) -> Result<(), InstructionError> { + let mut invoke_context = InvokeContext::new( + context.transaction_context, + context.program_cache, + context.environment_config, + trace.log_collector, + context.compute_budget.to_budget(), + context.compute_budget.to_cost(), + ); + + // Configure the next instruction frame for this invocation. + invoke_context + .transaction_context + .configure_next_instruction_for_tests( + instruction.program_id_index, + instruction.accounts.clone(), + instruction.data, + ) + .expect("failed to configure next instruction"); + + let program_id = invoke_context + .transaction_context + .get_key_of_account_at_index(instruction.program_id_index) + .cloned()?; + + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback.before_invocation( + &program_id, + instruction.data, + &instruction.accounts, + &invoke_context, + ); + + let result = if invoke_context.is_precompile(&program_id) { + invoke_context.process_precompile( + &program_id, + instruction.data, + std::iter::once(instruction.data), + ) + } else { + invoke_context.process_instruction(trace.compute_units_consumed, trace.execute_timings) + }; + + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback.after_invocation(&invoke_context); + + result + } +} diff --git a/harness/src/vm/mod.rs b/harness/src/vm/mod.rs new file mode 100644 index 00000000..e90d7ecd --- /dev/null +++ b/harness/src/vm/mod.rs @@ -0,0 +1,51 @@ +//! Virtual Machine API for using Mollusk with custom VMs. + +pub mod agave; + +#[cfg(feature = "invocation-inspect-callback")] +use crate::InvocationInspectCallback; +use { + solana_compute_budget::compute_budget::ComputeBudget, + solana_instruction_error::InstructionError, + solana_program_runtime::{ + invoke_context::EnvironmentConfig, loaded_programs::ProgramCacheForTxBatch, + }, + solana_svm_log_collector::LogCollector, + solana_svm_timings::ExecuteTimings, + solana_transaction_context::{InstructionAccount, TransactionContext}, + std::{cell::RefCell, rc::Rc}, +}; + +/// Context required to process a Solana instruction in a VM. +pub struct SolanaVMContext<'a> { + pub transaction_context: &'a mut TransactionContext, + pub program_cache: &'a mut ProgramCacheForTxBatch, + pub compute_budget: ComputeBudget, + pub environment_config: EnvironmentConfig<'a>, +} + +/// A Solana instruction to be processed by a VM. +pub struct SolanaVMInstruction<'a> { + pub program_id_index: u16, + pub accounts: Vec, + pub data: &'a [u8], +} + +/// Trace information about a Solana VM instruction invocation. +pub struct SolanaVMTrace<'a> { + pub compute_units_consumed: &'a mut u64, + pub execute_timings: &'a mut ExecuteTimings, + pub log_collector: Option>>, +} + +/// A virtual machine compatible with the Solana calling convention. +pub trait SolanaVM { + /// Process a Solana instruction. + fn process_instruction( + context: SolanaVMContext, + instruction: SolanaVMInstruction, + trace: SolanaVMTrace, + #[cfg(feature = "invocation-inspect-callback")] + invocation_inspect_callback: &dyn InvocationInspectCallback, + ) -> Result<(), InstructionError>; +}