mollusk_svm/
lib.rs

1//! # Mollusk
2//!
3//! Mollusk is a lightweight test harness for Solana programs. It provides a
4//! simple interface for testing Solana program executions in a minified
5//! Solana Virtual Machine (SVM) environment.
6//!
7//! It does not create any semblance of a validator runtime, but instead
8//! provisions a program execution pipeline directly from lower-level SVM
9//! components.
10//!
11//! In summary, the main processor - `process_instruction` - creates minified
12//! instances of Agave's program cache, transaction context, and invoke
13//! context. It uses these components to directly execute the provided
14//! program's ELF using the BPF Loader.
15//!
16//! Because it does not use AccountsDB, Bank, or any other large Agave
17//! components, the harness is exceptionally fast. However, it does require
18//! the user to provide an explicit list of accounts to use, since it has
19//! nowhere to load them from.
20//!
21//! The test environment can be further configured by adjusting the compute
22//! budget, feature set, or sysvars. These configurations are stored directly
23//! on the test harness (the `Mollusk` struct), but can be manipulated through
24//! a handful of helpers.
25//!
26//! Four main API methods are offered:
27//!
28//! * `process_instruction`: Process an instruction and return the result.
29//! * `process_and_validate_instruction`: Process an instruction and perform a
30//!   series of checks on the result, panicking if any checks fail.
31//! * `process_instruction_chain`: Process a chain of instructions and return
32//!   the result.
33//! * `process_and_validate_instruction_chain`: Process a chain of instructions
34//!   and perform a series of checks on each result, panicking if any checks
35//!   fail.
36//!
37//! ## Single Instructions
38//!
39//! Both `process_instruction` and `process_and_validate_instruction` deal with
40//! single instructions. The former simply processes the instruction and
41//! returns the result, while the latter processes the instruction and then
42//! performs a series of checks on the result. In both cases, the result is
43//! also returned.
44//!
45//! ```rust,ignore
46//! use {
47//!     mollusk_svm::Mollusk,
48//!     solana_sdk::{
49//!         account::Account,
50//!         instruction::{AccountMeta, Instruction},
51//!         pubkey::Pubkey,
52//!     },
53//! };
54//!
55//! let program_id = Pubkey::new_unique();
56//! let key1 = Pubkey::new_unique();
57//! let key2 = Pubkey::new_unique();
58//!
59//! let instruction = Instruction::new_with_bytes(
60//!     program_id,
61//!     &[],
62//!     vec![
63//!         AccountMeta::new(key1, false),
64//!         AccountMeta::new_readonly(key2, false),
65//!     ],
66//! );
67//!
68//! let accounts = vec![
69//!     (key1, Account::default()),
70//!     (key2, Account::default()),
71//! ];
72//!
73//! let mollusk = Mollusk::new(&program_id, "my_program");
74//!
75//! // Execute the instruction and get the result.
76//! let result = mollusk.process_instruction(&instruction, &accounts);
77//! ```
78//!
79//! To apply checks via `process_and_validate_instruction`, developers can use
80//! the `Check` enum, which provides a set of common checks.
81//!
82//! ```rust,ignore
83//! use {
84//!     mollusk_svm::{Mollusk, result::Check},
85//!     solana_sdk::{
86//!         account::Account,
87//!         instruction::{AccountMeta, Instruction},
88//!         pubkey::Pubkey
89//!         system_instruction,
90//!         system_program,
91//!     },
92//! };
93//!
94//! let sender = Pubkey::new_unique();
95//! let recipient = Pubkey::new_unique();
96//!
97//! let base_lamports = 100_000_000u64;
98//! let transfer_amount = 42_000u64;
99//!
100//! let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);
101//! let accounts = [
102//!     (
103//!         sender,
104//!         Account::new(base_lamports, 0, &system_program::id()),
105//!     ),
106//!     (
107//!         recipient,
108//!         Account::new(base_lamports, 0, &system_program::id()),
109//!     ),
110//! ];
111//! let checks = vec![
112//!     Check::success(),
113//!     Check::compute_units(system_processor::DEFAULT_COMPUTE_UNITS),
114//!     Check::account(&sender)
115//!         .lamports(base_lamports - transfer_amount)
116//!         .build(),
117//!     Check::account(&recipient)
118//!         .lamports(base_lamports + transfer_amount)
119//!         .build(),
120//! ];
121//!
122//! Mollusk::default().process_and_validate_instruction(
123//!     &instruction,
124//!     &accounts,
125//!     &checks,
126//! );
127//! ```
128//!
129//! Note: `Mollusk::default()` will create a new `Mollusk` instance without
130//! adding any provided BPF programs. It will still contain a subset of the
131//! default builtin programs. For more builtin programs, you can add them
132//! yourself or use the `all-builtins` feature.
133//!
134//! ## Instruction Chains
135//!
136//! Both `process_instruction_chain` and
137//! `process_and_validate_instruction_chain` deal with chains of instructions.
138//! The former processes each instruction in the chain and returns the final
139//! result, while the latter processes each instruction in the chain and then
140//! performs a series of checks on each result. In both cases, the final result
141//! is also returned.
142//!
143//! ```rust,ignore
144//! use {
145//!     mollusk_svm::Mollusk,
146//!     solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
147//! };
148//!
149//! let mollusk = Mollusk::default();
150//!
151//! let alice = Pubkey::new_unique();
152//! let bob = Pubkey::new_unique();
153//! let carol = Pubkey::new_unique();
154//! let dave = Pubkey::new_unique();
155//!
156//! let starting_lamports = 500_000_000;
157//!
158//! let alice_to_bob = 100_000_000;
159//! let bob_to_carol = 50_000_000;
160//! let bob_to_dave = 50_000_000;
161//!
162//! mollusk.process_instruction_chain(
163//!     &[
164//!         system_instruction::transfer(&alice, &bob, alice_to_bob),
165//!         system_instruction::transfer(&bob, &carol, bob_to_carol),
166//!         system_instruction::transfer(&bob, &dave, bob_to_dave),
167//!     ],
168//!     &[
169//!         (alice, system_account_with_lamports(starting_lamports)),
170//!         (bob, system_account_with_lamports(starting_lamports)),
171//!         (carol, system_account_with_lamports(starting_lamports)),
172//!         (dave, system_account_with_lamports(starting_lamports)),
173//!     ],
174//! );
175//! ```
176//!
177//! Just like with `process_and_validate_instruction`, developers can use the
178//! `Check` enum to apply checks via `process_and_validate_instruction_chain`.
179//! Notice that `process_and_validate_instruction_chain` takes a slice of
180//! tuples, where each tuple contains an instruction and a slice of checks.
181//! This allows the developer to apply specific checks to each instruction in
182//! the chain. The result returned by the method is the final result of the
183//! last instruction in the chain.
184//!
185//! ```rust,ignore
186//! use {
187//!     mollusk_svm::{Mollusk, result::Check},
188//!     solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
189//! };
190//!
191//! let mollusk = Mollusk::default();
192//!
193//! let alice = Pubkey::new_unique();
194//! let bob = Pubkey::new_unique();
195//! let carol = Pubkey::new_unique();
196//! let dave = Pubkey::new_unique();
197//!
198//! let starting_lamports = 500_000_000;
199//!
200//! let alice_to_bob = 100_000_000;
201//! let bob_to_carol = 50_000_000;
202//! let bob_to_dave = 50_000_000;
203//!
204//! mollusk.process_and_validate_instruction_chain(
205//!     &[
206//!         (
207//!             // 0: Alice to Bob
208//!             &system_instruction::transfer(&alice, &bob, alice_to_bob),
209//!             &[
210//!                 Check::success(),
211//!                 Check::account(&alice)
212//!                     .lamports(starting_lamports - alice_to_bob) // Alice pays
213//!                     .build(),
214//!                 Check::account(&bob)
215//!                     .lamports(starting_lamports + alice_to_bob) // Bob receives
216//!                     .build(),
217//!                 Check::account(&carol)
218//!                     .lamports(starting_lamports) // Unchanged
219//!                     .build(),
220//!                 Check::account(&dave)
221//!                     .lamports(starting_lamports) // Unchanged
222//!                     .build(),
223//!             ],
224//!         ),
225//!         (
226//!             // 1: Bob to Carol
227//!             &system_instruction::transfer(&bob, &carol, bob_to_carol),
228//!             &[
229//!                 Check::success(),
230//!                 Check::account(&alice)
231//!                     .lamports(starting_lamports - alice_to_bob) // Unchanged
232//!                     .build(),
233//!                 Check::account(&bob)
234//!                     .lamports(starting_lamports + alice_to_bob - bob_to_carol) // Bob pays
235//!                     .build(),
236//!                 Check::account(&carol)
237//!                     .lamports(starting_lamports + bob_to_carol) // Carol receives
238//!                     .build(),
239//!                 Check::account(&dave)
240//!                     .lamports(starting_lamports) // Unchanged
241//!                     .build(),
242//!             ],
243//!         ),
244//!         (
245//!             // 2: Bob to Dave
246//!             &system_instruction::transfer(&bob, &dave, bob_to_dave),
247//!             &[
248//!                 Check::success(),
249//!                 Check::account(&alice)
250//!                     .lamports(starting_lamports - alice_to_bob) // Unchanged
251//!                     .build(),
252//!                 Check::account(&bob)
253//!                     .lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave) // Bob pays
254//!                     .build(),
255//!                 Check::account(&carol)
256//!                     .lamports(starting_lamports + bob_to_carol) // Unchanged
257//!                     .build(),
258//!                 Check::account(&dave)
259//!                     .lamports(starting_lamports + bob_to_dave) // Dave receives
260//!                     .build(),
261//!             ],
262//!         ),
263//!     ],
264//!     &[
265//!         (alice, system_account_with_lamports(starting_lamports)),
266//!         (bob, system_account_with_lamports(starting_lamports)),
267//!         (carol, system_account_with_lamports(starting_lamports)),
268//!         (dave, system_account_with_lamports(starting_lamports)),
269//!     ],
270//! );
271//! ```
272//!
273//! It's important to understand that instruction chains _should not_ be
274//! considered equivalent to Solana transactions. Mollusk does not impose
275//! constraints on instruction chains, such as loaded account keys or size.
276//! Developers should recognize that instruction chains are primarily used for
277//! testing program execution.
278//!
279//! ## Stateful Testing with MolluskContext
280//!
281//! For complex testing scenarios that involve multiple instructions or require
282//! persistent state between calls, `MolluskContext` provides a stateful wrapper
283//! around `Mollusk`. It automatically manages an account store and provides the
284//! same API methods but without requiring explicit account management.
285//!
286//! ```rust,ignore
287//! use {
288//!     mollusk_svm::{Mollusk, account_store::AccountStore},
289//!     solana_account::Account,
290//!     solana_instruction::Instruction,
291//!     solana_pubkey::Pubkey,
292//!     solana_system_interface::instruction as system_instruction,
293//!     std::collections::HashMap,
294//! };
295//!
296//! // Simple in-memory account store implementation
297//! #[derive(Default)]
298//! struct InMemoryAccountStore {
299//!     accounts: HashMap<Pubkey, Account>,
300//! }
301//!
302//! impl AccountStore for InMemoryAccountStore {
303//!     fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
304//!         self.accounts.get(pubkey).cloned()
305//!     }
306//!
307//!     fn store_account(&mut self, pubkey: Pubkey, account: Account) {
308//!         self.accounts.insert(pubkey, account);
309//!     }
310//! }
311//!
312//! let mollusk = Mollusk::default();
313//! let context = mollusk.with_context(InMemoryAccountStore::default());
314//!
315//! let alice = Pubkey::new_unique();
316//! let bob = Pubkey::new_unique();
317//!
318//! // Execute instructions without managing accounts manually
319//! let instruction1 = system_instruction::transfer(&alice, &bob, 1_000_000);
320//! let result1 = context.process_instruction(&instruction1);
321//!
322//! let instruction2 = system_instruction::transfer(&bob, &alice, 500_000);
323//! let result2 = context.process_instruction(&instruction2);
324//!
325//! // Account state is automatically preserved between calls
326//! ```
327//!
328//! The `MolluskContext` API provides the same core methods as `Mollusk`:
329//!
330//! * `process_instruction`: Process an instruction with automatic account
331//!   management
332//! * `process_instruction_chain`: Process a chain of instructions
333//! * `process_and_validate_instruction`: Process and validate an instruction
334//! * `process_and_validate_instruction_chain`: Process and validate an
335//!   instruction chain
336//!
337//! All methods return `ContextResult` instead of `InstructionResult`, which
338//! omits the `resulting_accounts` field since accounts are managed by the
339//! context's account store.
340//!
341//! Note that `HashMap<Pubkey, Account>` implements `AccountStore` directly,
342//! so you can use it as a simple in-memory account store without needing
343//! to implement your own.
344//!
345//! ## Fixtures
346//!
347//! Mollusk also supports working with multiple kinds of fixtures, which can
348//! help expand testing capabilities. Note this is all gated behind either the
349//! `fuzz` or `fuzz-fd` feature flags.
350//!
351//! A fixture is a structured representation of a test case, containing the
352//! input data, the expected output data, and any additional context required
353//! to run the test. One fixture maps to one instruction.
354//!
355//! A classic use case for such fixtures is the act of testing two versions of
356//! a program against each other, to ensure the new version behaves as
357//! expected. The original version's test suite can be used to generate a set
358//! of fixtures, which can then be used as inputs to test the new version.
359//! Although you could also simply replace the program ELF file in the test
360//! suite to achieve a similar result, fixtures provide exhaustive coverage.
361//!
362//! ### Generating Fixtures from Mollusk Tests
363//!
364//! Mollusk is capable of generating fixtures from any defined test case. If
365//! the `EJECT_FUZZ_FIXTURES` environment variable is set during a test run,
366//! Mollusk will serialize every invocation of `process_instruction` into a
367//! fixture, using the provided inputs, current Mollusk configurations, and
368//! result returned. `EJECT_FUZZ_FIXTURES_JSON` can also be set to write the
369//! fixtures in JSON format.
370//!
371//! ```ignore
372//! EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
373//! ```
374//!
375//! Note that Mollusk currently supports two types of fixtures: Mollusk's own
376//! fixture layout and the fixture layout used by the Firedancer team. Both of
377//! these layouts stem from Protobuf definitions.
378//!
379//! These layouts live in separate crates, but a snippet of the Mollusk input
380//! data for a fixture can be found below:
381//!
382//! ```rust,ignore
383//! /// Instruction context fixture.
384//! pub struct Context {
385//!     /// The compute budget to use for the simulation.
386//!     pub compute_budget: ComputeBudget,
387//!     /// The feature set to use for the simulation.
388//!     pub feature_set: FeatureSet,
389//!     /// The runtime sysvars to use for the simulation.
390//!     pub sysvars: Sysvars,
391//!     /// The program ID of the program being invoked.
392//!     pub program_id: Pubkey,
393//!     /// Accounts to pass to the instruction.
394//!     pub instruction_accounts: Vec<AccountMeta>,
395//!     /// The instruction data.
396//!     pub instruction_data: Vec<u8>,
397//!     /// Input accounts with state.
398//!     pub accounts: Vec<(Pubkey, Account)>,
399//! }
400//! ```
401//!
402//! ### Loading and Executing Fixtures
403//!
404//! Mollusk can also execute fixtures, just like it can with instructions. The
405//! `process_fixture` method will process a fixture and return the result, while
406//! `process_and_validate_fixture` will process a fixture and compare the result
407//! against the fixture's effects.
408//!
409//! An additional method, `process_and_partially_validate_fixture`, allows
410//! developers to compare the result against the fixture's effects using a
411//! specific subset of checks, rather than the entire set of effects. This
412//! may be useful if you wish to ignore certain effects, such as compute units
413//! consumed.
414//!
415//! ```rust,ignore
416//! use {
417//!     mollusk_svm::{Mollusk, fuzz::check::FixtureCheck},
418//!     solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
419//!     std::{fs, path::Path},
420//! };
421//!
422//! let mollusk = Mollusk::default();
423//!
424//! for file in fs::read_dir(Path::new("fixtures-dir"))? {
425//!     let fixture = Fixture::load_from_blob_file(&entry?.file_name());
426//!
427//!     // Execute the fixture and apply partial checks.
428//!     mollusk.process_and_partially_validate_fixture(
429//!        &fixture,
430//!        &[
431//!            FixtureCheck::ProgramResult,
432//!            FixtureCheck::ReturnData,
433//!            FixtureCheck::all_resulting_accounts(),
434//!         ],
435//!     );
436//! }
437//! ```
438//!
439//! Fixtures can be loaded from files or decoded from raw blobs. These
440//! capabilities are provided by the respective fixture crates.
441
442pub mod account_store;
443mod compile_accounts;
444pub mod epoch_stake;
445pub mod file;
446#[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
447pub mod fuzz;
448pub mod program;
449pub mod sysvar;
450
451// Re-export result module from mollusk-svm-result crate
452pub use mollusk_svm_result as result;
453#[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
454use mollusk_svm_result::Compare;
455#[cfg(feature = "invocation-inspect-callback")]
456use solana_transaction_context::InstructionAccount;
457use {
458    crate::{
459        account_store::AccountStore, compile_accounts::CompiledAccounts, epoch_stake::EpochStake,
460        program::ProgramCache, sysvar::Sysvars,
461    },
462    agave_feature_set::FeatureSet,
463    mollusk_svm_error::error::{MolluskError, MolluskPanic},
464    mollusk_svm_result::{Check, CheckContext, Config, InstructionResult},
465    solana_account::Account,
466    solana_compute_budget::compute_budget::ComputeBudget,
467    solana_hash::Hash,
468    solana_instruction::{AccountMeta, Instruction},
469    solana_log_collector::LogCollector,
470    solana_precompile_error::PrecompileError,
471    solana_program_runtime::invoke_context::{EnvironmentConfig, InvokeContext},
472    solana_pubkey::Pubkey,
473    solana_svm_callback::InvokeContextCallback,
474    solana_timings::ExecuteTimings,
475    solana_transaction_context::TransactionContext,
476    std::{cell::RefCell, collections::HashSet, iter::once, rc::Rc},
477};
478
479pub(crate) const DEFAULT_LOADER_KEY: Pubkey = solana_sdk_ids::bpf_loader_upgradeable::id();
480
481/// The Mollusk API, providing a simple interface for testing Solana programs.
482///
483/// All fields can be manipulated through a handful of helper methods, but
484/// users can also directly access and modify them if they desire more control.
485pub struct Mollusk {
486    pub config: Config,
487    pub compute_budget: ComputeBudget,
488    pub epoch_stake: EpochStake,
489    pub feature_set: FeatureSet,
490    pub logger: Option<Rc<RefCell<LogCollector>>>,
491    pub program_cache: ProgramCache,
492    pub sysvars: Sysvars,
493
494    /// The callback which can be used to inspect invoke_context
495    /// and extract low-level information such as bpf traces, transaction
496    /// context, detailed timings, etc.
497    #[cfg(feature = "invocation-inspect-callback")]
498    pub invocation_inspect_callback: Box<dyn InvocationInspectCallback>,
499
500    /// This field stores the slot only to be able to convert to and from FD
501    /// fixtures and a Mollusk instance, since FD fixtures have a
502    /// "slot context". However, this field is functionally irrelevant for
503    /// instruction execution, since all slot-based information for on-chain
504    /// programs comes from the sysvars.
505    #[cfg(feature = "fuzz-fd")]
506    pub slot: u64,
507}
508
509#[cfg(feature = "invocation-inspect-callback")]
510pub trait InvocationInspectCallback {
511    fn before_invocation(
512        &self,
513        program_id: &Pubkey,
514        instruction_data: &[u8],
515        instruction_accounts: &[InstructionAccount],
516        invoke_context: &InvokeContext,
517    );
518
519    fn after_invocation(&self, invoke_context: &InvokeContext);
520}
521
522#[cfg(feature = "invocation-inspect-callback")]
523pub struct EmptyInvocationInspectCallback;
524
525#[cfg(feature = "invocation-inspect-callback")]
526impl InvocationInspectCallback for EmptyInvocationInspectCallback {
527    fn before_invocation(&self, _: &Pubkey, _: &[u8], _: &[InstructionAccount], _: &InvokeContext) {
528    }
529
530    fn after_invocation(&self, _: &InvokeContext) {}
531}
532
533impl Default for Mollusk {
534    fn default() -> Self {
535        #[rustfmt::skip]
536        solana_logger::setup_with_default(
537            "solana_rbpf::vm=debug,\
538             solana_runtime::message_processor=debug,\
539             solana_runtime::system_instruction_processor=trace",
540        );
541        let compute_budget = ComputeBudget::default();
542        #[cfg(feature = "fuzz")]
543        let feature_set = {
544            // Omit "test features" (they have the same u64 ID).
545            let mut fs = FeatureSet::all_enabled();
546            fs.active_mut()
547                .remove(&agave_feature_set::disable_sbpf_v0_execution::id());
548            fs.active_mut()
549                .remove(&agave_feature_set::reenable_sbpf_v0_execution::id());
550            fs
551        };
552        #[cfg(not(feature = "fuzz"))]
553        let feature_set = FeatureSet::all_enabled();
554        let program_cache = ProgramCache::new(&feature_set, &compute_budget);
555        Self {
556            config: Config::default(),
557            compute_budget,
558            epoch_stake: EpochStake::default(),
559            feature_set,
560            logger: None,
561            program_cache,
562            sysvars: Sysvars::default(),
563
564            #[cfg(feature = "invocation-inspect-callback")]
565            invocation_inspect_callback: Box::new(EmptyInvocationInspectCallback {}),
566
567            #[cfg(feature = "fuzz-fd")]
568            slot: 0,
569        }
570    }
571}
572
573impl CheckContext for Mollusk {
574    fn is_rent_exempt(&self, lamports: u64, space: usize, owner: Pubkey) -> bool {
575        owner.eq(&Pubkey::default()) && lamports == 0
576            || self.sysvars.rent.is_exempt(lamports, space)
577    }
578}
579
580struct MolluskInvokeContextCallback<'a> {
581    feature_set: &'a FeatureSet,
582    epoch_stake: &'a EpochStake,
583}
584
585impl InvokeContextCallback for MolluskInvokeContextCallback<'_> {
586    fn get_epoch_stake(&self) -> u64 {
587        self.epoch_stake.values().sum()
588    }
589
590    fn get_epoch_stake_for_vote_account(&self, vote_address: &Pubkey) -> u64 {
591        self.epoch_stake.get(vote_address).copied().unwrap_or(0)
592    }
593
594    fn is_precompile(&self, program_id: &Pubkey) -> bool {
595        agave_precompiles::is_precompile(program_id, |feature_id| {
596            self.feature_set.is_active(feature_id)
597        })
598    }
599
600    fn process_precompile(
601        &self,
602        program_id: &Pubkey,
603        data: &[u8],
604        instruction_datas: Vec<&[u8]>,
605    ) -> Result<(), PrecompileError> {
606        if let Some(precompile) = agave_precompiles::get_precompile(program_id, |feature_id| {
607            self.feature_set.is_active(feature_id)
608        }) {
609            precompile.verify(data, &instruction_datas, self.feature_set)
610        } else {
611            Err(PrecompileError::InvalidPublicKey)
612        }
613    }
614}
615
616impl Mollusk {
617    /// Create a new Mollusk instance containing the provided program.
618    ///
619    /// Attempts to load the program's ELF file from the default search paths.
620    /// Once loaded, adds the program to the program cache and returns the
621    /// newly created Mollusk instance.
622    ///
623    /// # Default Search Paths
624    ///
625    /// The following locations are checked in order:
626    ///
627    /// - `tests/fixtures`
628    /// - The directory specified by the `BPF_OUT_DIR` environment variable
629    /// - The directory specified by the `SBF_OUT_DIR` environment variable
630    /// - The current working directory
631    pub fn new(program_id: &Pubkey, program_name: &str) -> Self {
632        let mut mollusk = Self::default();
633        mollusk.add_program(program_id, program_name, &DEFAULT_LOADER_KEY);
634        mollusk
635    }
636
637    /// Add a program to the test environment.
638    ///
639    /// If you intend to CPI to a program, this is likely what you want to use.
640    pub fn add_program(&mut self, program_id: &Pubkey, program_name: &str, loader_key: &Pubkey) {
641        let elf = file::load_program_elf(program_name);
642        self.add_program_with_elf_and_loader(program_id, &elf, loader_key);
643    }
644
645    /// Add a program to the test environment using a provided ELF under a
646    /// specific loader.
647    ///
648    /// If you intend to CPI to a program, this is likely what you want to use.
649    pub fn add_program_with_elf_and_loader(
650        &mut self,
651        program_id: &Pubkey,
652        elf: &[u8],
653        loader_key: &Pubkey,
654    ) {
655        self.program_cache.add_program(program_id, loader_key, elf);
656    }
657
658    /// Warp the test environment to a slot by updating sysvars.
659    pub fn warp_to_slot(&mut self, slot: u64) {
660        self.sysvars.warp_to_slot(slot)
661    }
662
663    /// Process an instruction using the minified Solana Virtual Machine (SVM)
664    /// environment. Simply returns the result.
665    pub fn process_instruction(
666        &self,
667        instruction: &Instruction,
668        accounts: &[(Pubkey, Account)],
669    ) -> InstructionResult {
670        let mut compute_units_consumed = 0;
671        let mut timings = ExecuteTimings::default();
672
673        let loader_key = if crate::program::precompile_keys::is_precompile(&instruction.program_id)
674        {
675            crate::program::loader_keys::NATIVE_LOADER
676        } else {
677            self.program_cache
678                .load_program(&instruction.program_id)
679                .or_panic_with(MolluskError::ProgramNotCached(&instruction.program_id))
680                .account_owner()
681        };
682
683        let CompiledAccounts {
684            program_id_index,
685            instruction_accounts,
686            transaction_accounts,
687        } = crate::compile_accounts::compile_accounts(instruction, accounts, loader_key);
688
689        let mut transaction_context = TransactionContext::new(
690            transaction_accounts,
691            self.sysvars.rent.clone(),
692            self.compute_budget.max_instruction_stack_depth,
693            self.compute_budget.max_instruction_trace_length,
694        );
695
696        let invoke_result = {
697            let mut program_cache = self.program_cache.cache();
698            let callback = MolluskInvokeContextCallback {
699                epoch_stake: &self.epoch_stake,
700                feature_set: &self.feature_set,
701            };
702            let runtime_features = self.feature_set.runtime_features();
703            let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts);
704            let mut invoke_context = InvokeContext::new(
705                &mut transaction_context,
706                &mut program_cache,
707                EnvironmentConfig::new(
708                    Hash::default(),
709                    /* blockhash_lamports_per_signature */ 5000, // The default value
710                    &callback,
711                    &runtime_features,
712                    &sysvar_cache,
713                ),
714                self.logger.clone(),
715                self.compute_budget.to_budget(),
716                self.compute_budget.to_cost(),
717            );
718
719            #[cfg(feature = "invocation-inspect-callback")]
720            self.invocation_inspect_callback.before_invocation(
721                &instruction.program_id,
722                &instruction.data,
723                &instruction_accounts,
724                &invoke_context,
725            );
726
727            let result = if invoke_context.is_precompile(&instruction.program_id) {
728                invoke_context.process_precompile(
729                    &instruction.program_id,
730                    &instruction.data,
731                    &instruction_accounts,
732                    &[program_id_index],
733                    std::iter::once(instruction.data.as_ref()),
734                )
735            } else {
736                invoke_context.process_instruction(
737                    &instruction.data,
738                    &instruction_accounts,
739                    &[program_id_index],
740                    &mut compute_units_consumed,
741                    &mut timings,
742                )
743            };
744
745            #[cfg(feature = "invocation-inspect-callback")]
746            self.invocation_inspect_callback
747                .after_invocation(&invoke_context);
748
749            result
750        };
751
752        let return_data = transaction_context.get_return_data().1.to_vec();
753
754        let resulting_accounts: Vec<(Pubkey, Account)> = if invoke_result.is_ok() {
755            accounts
756                .iter()
757                .map(|(pubkey, account)| {
758                    transaction_context
759                        .find_index_of_account(pubkey)
760                        .map(|index| {
761                            let resulting_account = transaction_context
762                                .get_account_at_index(index)
763                                .unwrap()
764                                .borrow()
765                                .clone()
766                                .into();
767                            (*pubkey, resulting_account)
768                        })
769                        .unwrap_or((*pubkey, account.clone()))
770                })
771                .collect()
772        } else {
773            accounts.to_vec()
774        };
775
776        InstructionResult {
777            compute_units_consumed,
778            execution_time: timings.details.execute_us.0,
779            program_result: invoke_result.clone().into(),
780            raw_result: invoke_result,
781            return_data,
782            resulting_accounts,
783        }
784    }
785
786    /// Process a chain of instructions using the minified Solana Virtual
787    /// Machine (SVM) environment. The returned result is an
788    /// `InstructionResult`, containing:
789    ///
790    /// * `compute_units_consumed`: The total compute units consumed across all
791    ///   instructions.
792    /// * `execution_time`: The total execution time across all instructions.
793    /// * `program_result`: The program result of the _last_ instruction.
794    /// * `resulting_accounts`: The resulting accounts after the _last_
795    ///   instruction.
796    pub fn process_instruction_chain(
797        &self,
798        instructions: &[Instruction],
799        accounts: &[(Pubkey, Account)],
800    ) -> InstructionResult {
801        let mut result = InstructionResult {
802            resulting_accounts: accounts.to_vec(),
803            ..Default::default()
804        };
805
806        for instruction in instructions {
807            let this_result = self.process_instruction(instruction, &result.resulting_accounts);
808
809            result.absorb(this_result);
810
811            if result.program_result.is_err() {
812                break;
813            }
814        }
815
816        result
817    }
818
819    /// Process an instruction using the minified Solana Virtual Machine (SVM)
820    /// environment, then perform checks on the result. Panics if any checks
821    /// fail.
822    ///
823    /// For `fuzz` feature only:
824    ///
825    /// If the `EJECT_FUZZ_FIXTURES` environment variable is set, this function
826    /// will convert the provided test to a fuzz fixture and write it to the
827    /// provided directory.
828    ///
829    /// ```ignore
830    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
831    /// ```
832    ///
833    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
834    /// JSON format.
835    ///
836    /// The `fuzz-fd` feature works the same way, but the variables require
837    /// the `_FD` suffix, in case both features are active together
838    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
839    /// fixtures, which are structured a bit differently than Mollusk's own
840    /// protobuf layouts.
841    pub fn process_and_validate_instruction(
842        &self,
843        instruction: &Instruction,
844        accounts: &[(Pubkey, Account)],
845        checks: &[Check],
846    ) -> InstructionResult {
847        let result = self.process_instruction(instruction, accounts);
848
849        #[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
850        fuzz::generate_fixtures_from_mollusk_test(self, instruction, accounts, &result);
851
852        result.run_checks(checks, &self.config, self);
853        result
854    }
855
856    /// Process a chain of instructions using the minified Solana Virtual
857    /// Machine (SVM) environment, then perform checks on the result.
858    /// Panics if any checks fail.
859    ///
860    /// For `fuzz` feature only:
861    ///
862    /// Similar to `process_and_validate_instruction`, if the
863    /// `EJECT_FUZZ_FIXTURES` environment variable is set, this function will
864    /// convert the provided test to a set of fuzz fixtures - each of which
865    /// corresponds to a single instruction in the chain - and write them to
866    /// the provided directory.
867    ///
868    /// ```ignore
869    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
870    /// ```
871    ///
872    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
873    /// JSON format.
874    ///
875    /// The `fuzz-fd` feature works the same way, but the variables require
876    /// the `_FD` suffix, in case both features are active together
877    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
878    /// fixtures, which are structured a bit differently than Mollusk's own
879    /// protobuf layouts.
880    pub fn process_and_validate_instruction_chain(
881        &self,
882        instructions: &[(&Instruction, &[Check])],
883        accounts: &[(Pubkey, Account)],
884    ) -> InstructionResult {
885        let mut result = InstructionResult {
886            resulting_accounts: accounts.to_vec(),
887            ..Default::default()
888        };
889
890        for (instruction, checks) in instructions.iter() {
891            let this_result = self.process_and_validate_instruction(
892                instruction,
893                &result.resulting_accounts,
894                checks,
895            );
896
897            result.absorb(this_result);
898
899            if result.program_result.is_err() {
900                break;
901            }
902        }
903
904        result
905    }
906
907    #[cfg(feature = "fuzz")]
908    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
909    /// environment.
910    ///
911    /// Fixtures provide an API to `decode` a raw blob, as well as read
912    /// fixtures from files. Those fixtures can then be provided to this
913    /// function to process them and get a Mollusk result.
914    ///
915    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
916    /// into the test environment will alter `Mollusk` values, such as compute
917    /// budget and sysvars. However, the program cache remains unchanged.
918    ///
919    /// Therefore, developers can provision a `Mollusk` instance, set up their
920    /// desired program cache, and then run a series of fixtures against that
921    /// `Mollusk` instance (and cache).
922    pub fn process_fixture(
923        &mut self,
924        fixture: &mollusk_svm_fuzz_fixture::Fixture,
925    ) -> InstructionResult {
926        let fuzz::mollusk::ParsedFixtureContext {
927            accounts,
928            compute_budget,
929            feature_set,
930            instruction,
931            sysvars,
932        } = fuzz::mollusk::parse_fixture_context(&fixture.input);
933        self.compute_budget = compute_budget;
934        self.feature_set = feature_set;
935        self.sysvars = sysvars;
936        self.process_instruction(&instruction, &accounts)
937    }
938
939    #[cfg(feature = "fuzz")]
940    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
941    /// environment and compare the result against the fixture's effects.
942    ///
943    /// Fixtures provide an API to `decode` a raw blob, as well as read
944    /// fixtures from files. Those fixtures can then be provided to this
945    /// function to process them and get a Mollusk result.
946    ///
947    ///
948    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
949    /// into the test environment will alter `Mollusk` values, such as compute
950    /// budget and sysvars. However, the program cache remains unchanged.
951    ///
952    /// Therefore, developers can provision a `Mollusk` instance, set up their
953    /// desired program cache, and then run a series of fixtures against that
954    /// `Mollusk` instance (and cache).
955    ///
956    /// Note: To compare the result against the entire fixture effects, pass
957    /// `&[FixtureCheck::All]` for `checks`.
958    pub fn process_and_validate_fixture(
959        &mut self,
960        fixture: &mollusk_svm_fuzz_fixture::Fixture,
961    ) -> InstructionResult {
962        let result = self.process_fixture(fixture);
963        InstructionResult::from(&fixture.output).compare_with_config(
964            &result,
965            &Compare::everything(),
966            &self.config,
967        );
968        result
969    }
970
971    #[cfg(feature = "fuzz")]
972    /// a specific set of checks.
973    ///
974    /// This is useful for when you may not want to compare the entire effects,
975    /// such as omitting comparisons of compute units consumed.
976    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
977    /// environment and compare the result against the fixture's effects using
978    /// a specific set of checks.
979    ///
980    /// This is useful for when you may not want to compare the entire effects,
981    /// such as omitting comparisons of compute units consumed.
982    ///
983    /// Fixtures provide an API to `decode` a raw blob, as well as read
984    /// fixtures from files. Those fixtures can then be provided to this
985    /// function to process them and get a Mollusk result.
986    ///
987    ///
988    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
989    /// into the test environment will alter `Mollusk` values, such as compute
990    /// budget and sysvars. However, the program cache remains unchanged.
991    ///
992    /// Therefore, developers can provision a `Mollusk` instance, set up their
993    /// desired program cache, and then run a series of fixtures against that
994    /// `Mollusk` instance (and cache).
995    ///
996    /// Note: To compare the result against the entire fixture effects, pass
997    /// `&[FixtureCheck::All]` for `checks`.
998    pub fn process_and_partially_validate_fixture(
999        &mut self,
1000        fixture: &mollusk_svm_fuzz_fixture::Fixture,
1001        checks: &[Compare],
1002    ) -> InstructionResult {
1003        let result = self.process_fixture(fixture);
1004        let expected = InstructionResult::from(&fixture.output);
1005        result.compare_with_config(&expected, checks, &self.config);
1006        result
1007    }
1008
1009    #[cfg(feature = "fuzz-fd")]
1010    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1011    /// Machine (SVM) environment.
1012    ///
1013    /// Fixtures provide an API to `decode` a raw blob, as well as read
1014    /// fixtures from files. Those fixtures can then be provided to this
1015    /// function to process them and get a Mollusk result.
1016    ///
1017    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1018    /// into the test environment will alter `Mollusk` values, such as compute
1019    /// budget and sysvars. However, the program cache remains unchanged.
1020    ///
1021    /// Therefore, developers can provision a `Mollusk` instance, set up their
1022    /// desired program cache, and then run a series of fixtures against that
1023    /// `Mollusk` instance (and cache).
1024    pub fn process_firedancer_fixture(
1025        &mut self,
1026        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1027    ) -> InstructionResult {
1028        let fuzz::firedancer::ParsedFixtureContext {
1029            accounts,
1030            compute_budget,
1031            feature_set,
1032            instruction,
1033            slot,
1034        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1035        self.compute_budget = compute_budget;
1036        self.feature_set = feature_set;
1037        self.slot = slot;
1038        self.process_instruction(&instruction, &accounts)
1039    }
1040
1041    #[cfg(feature = "fuzz-fd")]
1042    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1043    /// Machine (SVM) environment and compare the result against the
1044    /// fixture's effects.
1045    ///
1046    /// Fixtures provide an API to `decode` a raw blob, as well as read
1047    /// fixtures from files. Those fixtures can then be provided to this
1048    /// function to process them and get a Mollusk result.
1049    ///
1050    ///
1051    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1052    /// into the test environment will alter `Mollusk` values, such as compute
1053    /// budget and sysvars. However, the program cache remains unchanged.
1054    ///
1055    /// Therefore, developers can provision a `Mollusk` instance, set up their
1056    /// desired program cache, and then run a series of fixtures against that
1057    /// `Mollusk` instance (and cache).
1058    ///
1059    /// Note: To compare the result against the entire fixture effects, pass
1060    /// `&[FixtureCheck::All]` for `checks`.
1061    pub fn process_and_validate_firedancer_fixture(
1062        &mut self,
1063        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1064    ) -> InstructionResult {
1065        let fuzz::firedancer::ParsedFixtureContext {
1066            accounts,
1067            compute_budget,
1068            feature_set,
1069            instruction,
1070            slot,
1071        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1072        self.compute_budget = compute_budget;
1073        self.feature_set = feature_set;
1074        self.slot = slot;
1075
1076        let result = self.process_instruction(&instruction, &accounts);
1077        let expected_result = fuzz::firedancer::parse_fixture_effects(
1078            &accounts,
1079            self.compute_budget.compute_unit_limit,
1080            &fixture.output,
1081        );
1082
1083        expected_result.compare_with_config(&result, &Compare::everything(), &self.config);
1084        result
1085    }
1086
1087    #[cfg(feature = "fuzz-fd")]
1088    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1089    /// Machine (SVM) environment and compare the result against the
1090    /// fixture's effects using a specific set of checks.
1091    ///
1092    /// This is useful for when you may not want to compare the entire effects,
1093    /// such as omitting comparisons of compute units consumed.
1094    ///
1095    /// Fixtures provide an API to `decode` a raw blob, as well as read
1096    /// fixtures from files. Those fixtures can then be provided to this
1097    /// function to process them and get a Mollusk result.
1098    ///
1099    ///
1100    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1101    /// into the test environment will alter `Mollusk` values, such as compute
1102    /// budget and sysvars. However, the program cache remains unchanged.
1103    ///
1104    /// Therefore, developers can provision a `Mollusk` instance, set up their
1105    /// desired program cache, and then run a series of fixtures against that
1106    /// `Mollusk` instance (and cache).
1107    ///
1108    /// Note: To compare the result against the entire fixture effects, pass
1109    /// `&[FixtureCheck::All]` for `checks`.
1110    pub fn process_and_partially_validate_firedancer_fixture(
1111        &mut self,
1112        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1113        checks: &[Compare],
1114    ) -> InstructionResult {
1115        let fuzz::firedancer::ParsedFixtureContext {
1116            accounts,
1117            compute_budget,
1118            feature_set,
1119            instruction,
1120            slot,
1121        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1122        self.compute_budget = compute_budget;
1123        self.feature_set = feature_set;
1124        self.slot = slot;
1125
1126        let result = self.process_instruction(&instruction, &accounts);
1127        let expected = fuzz::firedancer::parse_fixture_effects(
1128            &accounts,
1129            self.compute_budget.compute_unit_limit,
1130            &fixture.output,
1131        );
1132
1133        result.compare_with_config(&expected, checks, &self.config);
1134        result
1135    }
1136
1137    /// Convert this `Mollusk` instance into a `MolluskContext` for stateful
1138    /// testing.
1139    ///
1140    /// Creates a context wrapper that manages persistent state between
1141    /// instruction executions, starting with the provided account store.
1142    ///
1143    /// See [`MolluskContext`] for more details on how to use it.
1144    pub fn with_context<AS: AccountStore>(self, mut account_store: AS) -> MolluskContext<AS> {
1145        // For convenience, load all program accounts into the account store,
1146        // but only if they don't exist.
1147        self.program_cache
1148            .get_all_keyed_program_accounts()
1149            .into_iter()
1150            .for_each(|(pubkey, account)| {
1151                if account_store.get_account(&pubkey).is_none() {
1152                    account_store.store_account(pubkey, account);
1153                }
1154            });
1155        MolluskContext {
1156            mollusk: self,
1157            account_store: Rc::new(RefCell::new(account_store)),
1158            hydrate_store: true, // <-- Default
1159        }
1160    }
1161}
1162
1163/// A stateful wrapper around `Mollusk` that provides additional context and
1164/// convenience features for testing programs.
1165///
1166/// `MolluskContext` maintains persistent state between instruction executions,
1167/// starting with an account store that automatically manages account
1168/// lifecycles. This makes it ideal for complex testing scenarios involving
1169/// multiple instructions, instruction chains, and stateful program
1170/// interactions.
1171///
1172/// Note: Account state is only persisted if the instruction execution
1173/// was successful. If an instruction fails, the account state will not
1174/// be updated.
1175///
1176/// The API is functionally identical to `Mollusk` but with enhanced state
1177/// management and a streamlined interface. Namely, the input `accounts` slice
1178/// is no longer required, and the returned result does not contain a
1179/// `resulting_accounts` field.
1180pub struct MolluskContext<AS: AccountStore> {
1181    pub mollusk: Mollusk,
1182    pub account_store: Rc<RefCell<AS>>,
1183    pub hydrate_store: bool,
1184}
1185
1186impl<AS: AccountStore> MolluskContext<AS> {
1187    fn load_accounts_for_instructions<'a>(
1188        &self,
1189        instructions: impl Iterator<Item = &'a Instruction>,
1190    ) -> Vec<(Pubkey, Account)> {
1191        let mut accounts = Vec::new();
1192
1193        // If hydration is enabled, add sysvars and program accounts regardless
1194        // of whether or not they exist already.
1195        if self.hydrate_store {
1196            self.mollusk
1197                .program_cache
1198                .get_all_keyed_program_accounts()
1199                .into_iter()
1200                .chain(self.mollusk.sysvars.get_all_keyed_sysvar_accounts())
1201                .for_each(|(pubkey, account)| {
1202                    accounts.push((pubkey, account));
1203                });
1204        }
1205
1206        // Regardless of hydration, only add an account if the caller hasn't
1207        // already loaded it into the store.
1208        let mut seen = HashSet::new();
1209        let store = self.account_store.borrow();
1210        instructions.for_each(|instruction| {
1211            instruction
1212                .accounts
1213                .iter()
1214                .for_each(|AccountMeta { pubkey, .. }| {
1215                    if seen.insert(*pubkey) {
1216                        // First try to load theirs, then see if it's a sysvar,
1217                        // then see if it's a cached program, then apply the
1218                        // default.
1219                        let account = store.get_account(pubkey).unwrap_or_else(|| {
1220                            self.mollusk
1221                                .sysvars
1222                                .maybe_create_sysvar_account(pubkey)
1223                                .unwrap_or_else(|| {
1224                                    self.mollusk
1225                                        .program_cache
1226                                        .maybe_create_program_account(pubkey)
1227                                        .unwrap_or_else(|| store.default_account(pubkey))
1228                                })
1229                        });
1230                        accounts.push((*pubkey, account));
1231                    }
1232                });
1233        });
1234        accounts
1235    }
1236
1237    fn consume_mollusk_result(&self, result: &InstructionResult) {
1238        if result.program_result.is_ok() {
1239            // Only store resulting accounts if the result was success.
1240            let mut store = self.account_store.borrow_mut();
1241            for (pubkey, account) in result.resulting_accounts.iter() {
1242                store.store_account(*pubkey, account.clone());
1243            }
1244        }
1245    }
1246
1247    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1248    /// environment. Simply returns the result.
1249    pub fn process_instruction(&self, instruction: &Instruction) -> InstructionResult {
1250        let accounts = self.load_accounts_for_instructions(once(instruction));
1251        let result = self.mollusk.process_instruction(instruction, &accounts);
1252        self.consume_mollusk_result(&result);
1253        result
1254    }
1255
1256    /// Process a chain of instructions using the minified Solana Virtual
1257    /// Machine (SVM) environment.
1258    pub fn process_instruction_chain(&self, instructions: &[Instruction]) -> InstructionResult {
1259        let accounts = self.load_accounts_for_instructions(instructions.iter());
1260        let result = self
1261            .mollusk
1262            .process_instruction_chain(instructions, &accounts);
1263        self.consume_mollusk_result(&result);
1264        result
1265    }
1266
1267    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1268    /// environment, then perform checks on the result.
1269    pub fn process_and_validate_instruction(
1270        &self,
1271        instruction: &Instruction,
1272        checks: &[Check],
1273    ) -> InstructionResult {
1274        let accounts = self.load_accounts_for_instructions(once(instruction));
1275        let result = self
1276            .mollusk
1277            .process_and_validate_instruction(instruction, &accounts, checks);
1278        self.consume_mollusk_result(&result);
1279        result
1280    }
1281
1282    /// Process a chain of instructions using the minified Solana Virtual
1283    /// Machine (SVM) environment, then perform checks on the result.
1284    pub fn process_and_validate_instruction_chain(
1285        &self,
1286        instructions: &[(&Instruction, &[Check])],
1287    ) -> InstructionResult {
1288        let accounts = self.load_accounts_for_instructions(
1289            instructions.iter().map(|(instruction, _)| *instruction),
1290        );
1291        let result = self
1292            .mollusk
1293            .process_and_validate_instruction_chain(instructions, &accounts);
1294        self.consume_mollusk_result(&result);
1295        result
1296    }
1297}