Skip to main content

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 instructions_sysvar;
449pub mod program;
450#[cfg(feature = "register-tracing")]
451pub mod register_tracing;
452pub mod sysvar;
453
454#[cfg(feature = "register-tracing")]
455use crate::register_tracing::DefaultRegisterTracingCallback;
456// Re-export result module from mollusk-svm-result crate
457pub use mollusk_svm_result as result;
458#[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
459use mollusk_svm_result::Compare;
460#[cfg(feature = "precompiles")]
461use solana_precompile_error::PrecompileError;
462#[cfg(feature = "invocation-inspect-callback")]
463use solana_transaction_context::InstructionAccount;
464use {
465    crate::{
466        account_store::AccountStore, epoch_stake::EpochStake, program::ProgramCache,
467        sysvar::Sysvars,
468    },
469    agave_feature_set::FeatureSet,
470    agave_syscalls::{
471        create_program_runtime_environment_v1, create_program_runtime_environment_v2,
472    },
473    mollusk_svm_error::error::{MolluskError, MolluskPanic},
474    mollusk_svm_result::{
475        types::{TransactionProgramResult, TransactionResult},
476        Check, CheckContext, Config, InstructionResult,
477    },
478    solana_account::{Account, AccountSharedData, ReadableAccount},
479    solana_compute_budget::compute_budget::ComputeBudget,
480    solana_hash::Hash,
481    solana_instruction::{AccountMeta, Instruction},
482    solana_instruction_error::InstructionError,
483    solana_message::SanitizedMessage,
484    solana_program_error::ProgramError,
485    solana_program_runtime::{
486        invoke_context::{EnvironmentConfig, InvokeContext},
487        loaded_programs::ProgramRuntimeEnvironments,
488        sysvar_cache::SysvarCache,
489    },
490    solana_pubkey::Pubkey,
491    solana_svm_callback::InvokeContextCallback,
492    solana_svm_log_collector::LogCollector,
493    solana_svm_timings::ExecuteTimings,
494    solana_svm_transaction::instruction::SVMInstruction,
495    solana_transaction_context::{IndexOfAccount, TransactionContext},
496    solana_transaction_error::TransactionError,
497    std::{
498        cell::RefCell,
499        collections::{HashMap, HashSet},
500        iter::once,
501        rc::Rc,
502        sync::Arc,
503    },
504};
505#[cfg(feature = "inner-instructions")]
506use {
507    solana_message::compiled_instruction::CompiledInstruction,
508    solana_transaction_status_client_types::InnerInstruction,
509};
510
511pub(crate) const DEFAULT_LOADER_KEY: Pubkey = solana_sdk_ids::bpf_loader_upgradeable::id();
512
513/// The Mollusk API, providing a simple interface for testing Solana programs.
514///
515/// All fields can be manipulated through a handful of helper methods, but
516/// users can also directly access and modify them if they desire more control.
517pub struct Mollusk {
518    pub config: Config,
519    pub compute_budget: ComputeBudget,
520    pub epoch_stake: EpochStake,
521    pub feature_set: FeatureSet,
522    pub logger: Option<Rc<RefCell<LogCollector>>>,
523    pub program_cache: ProgramCache,
524    pub sysvars: Sysvars,
525
526    /// The callback which can be used to inspect invoke_context
527    /// and extract low-level information such as bpf traces, transaction
528    /// context, detailed timings, etc.
529    #[cfg(feature = "invocation-inspect-callback")]
530    pub invocation_inspect_callback: Box<dyn InvocationInspectCallback>,
531
532    /// Dictates whether or not register tracing was enabled.
533    /// Provided as input to the invocation inspect callback for potential
534    /// register trace consumption.
535    #[cfg(feature = "invocation-inspect-callback")]
536    enable_register_tracing: bool,
537
538    /// This field stores the slot only to be able to convert to and from FD
539    /// fixtures and a Mollusk instance, since FD fixtures have a
540    /// "slot context". However, this field is functionally irrelevant for
541    /// instruction execution, since all slot-based information for on-chain
542    /// programs comes from the sysvars.
543    #[cfg(feature = "fuzz-fd")]
544    pub slot: u64,
545}
546
547#[cfg(feature = "invocation-inspect-callback")]
548pub trait InvocationInspectCallback {
549    fn before_invocation(
550        &self,
551        mollusk: &Mollusk,
552        program_id: &Pubkey,
553        instruction_data: &[u8],
554        instruction_accounts: &[InstructionAccount],
555        invoke_context: &InvokeContext,
556    );
557
558    fn after_invocation(
559        &self,
560        mollusk: &Mollusk,
561        invoke_context: &InvokeContext,
562        register_tracing_enabled: bool,
563    );
564}
565
566#[cfg(feature = "invocation-inspect-callback")]
567pub struct EmptyInvocationInspectCallback;
568
569#[cfg(feature = "invocation-inspect-callback")]
570impl InvocationInspectCallback for EmptyInvocationInspectCallback {
571    fn before_invocation(
572        &self,
573        _: &Mollusk,
574        _: &Pubkey,
575        _: &[u8],
576        _: &[InstructionAccount],
577        _: &InvokeContext,
578    ) {
579    }
580
581    fn after_invocation(&self, _: &Mollusk, _: &InvokeContext, _register_tracing_enabled: bool) {}
582}
583
584impl Default for Mollusk {
585    fn default() -> Self {
586        let _enable_register_tracing = false;
587
588        // Allow users to virtually get register tracing data without
589        // doing any changes to their code provided `SBF_TRACE_DIR` is set.
590        #[cfg(feature = "register-tracing")]
591        let _enable_register_tracing = std::env::var("SBF_TRACE_DIR").is_ok();
592
593        Self::new_inner(_enable_register_tracing)
594    }
595}
596
597impl CheckContext for Mollusk {
598    fn is_rent_exempt(&self, lamports: u64, space: usize, owner: Pubkey) -> bool {
599        owner.eq(&Pubkey::default()) && lamports == 0
600            || self.sysvars.rent.is_exempt(lamports, space)
601    }
602}
603
604struct MolluskInvokeContextCallback<'a> {
605    #[cfg_attr(not(feature = "precompiles"), allow(dead_code))]
606    feature_set: &'a FeatureSet,
607    epoch_stake: &'a EpochStake,
608}
609
610impl InvokeContextCallback for MolluskInvokeContextCallback<'_> {
611    fn get_epoch_stake(&self) -> u64 {
612        self.epoch_stake.values().sum()
613    }
614
615    fn get_epoch_stake_for_vote_account(&self, vote_address: &Pubkey) -> u64 {
616        self.epoch_stake.get(vote_address).copied().unwrap_or(0)
617    }
618
619    #[cfg(feature = "precompiles")]
620    fn is_precompile(&self, program_id: &Pubkey) -> bool {
621        agave_precompiles::is_precompile(program_id, |feature_id| {
622            self.feature_set.is_active(feature_id)
623        })
624    }
625
626    #[cfg(not(feature = "precompiles"))]
627    fn is_precompile(&self, _program_id: &Pubkey) -> bool {
628        false
629    }
630
631    #[cfg(feature = "precompiles")]
632    fn process_precompile(
633        &self,
634        program_id: &Pubkey,
635        data: &[u8],
636        instruction_datas: Vec<&[u8]>,
637    ) -> Result<(), PrecompileError> {
638        if let Some(precompile) = agave_precompiles::get_precompile(program_id, |feature_id| {
639            self.feature_set.is_active(feature_id)
640        }) {
641            precompile.verify(data, &instruction_datas, self.feature_set)
642        } else {
643            Err(PrecompileError::InvalidPublicKey)
644        }
645    }
646
647    #[cfg(not(feature = "precompiles"))]
648    fn process_precompile(
649        &self,
650        _program_id: &Pubkey,
651        _data: &[u8],
652        _instruction_datas: Vec<&[u8]>,
653    ) -> Result<(), solana_precompile_error::PrecompileError> {
654        panic!("precompiles feature not enabled");
655    }
656}
657
658struct MessageResult {
659    /// The number of compute units consumed by the transaction.
660    pub compute_units_consumed: u64,
661    /// The time taken to execute the transaction, in microseconds.
662    pub execution_time: u64,
663    /// The raw result of the transaction's execution.
664    pub raw_result: Result<(), TransactionError>,
665    /// The return data produced by the transaction, if any.
666    pub return_data: Vec<u8>,
667    /// Inner instructions (CPIs) invoked during the transaction execution.
668    ///
669    /// Each entry represents a cross-program invocation made by the program,
670    /// including the invoked instruction and the stack height at which it
671    /// was called.
672    #[cfg(feature = "inner-instructions")]
673    pub inner_instructions: Vec<Vec<InnerInstruction>>,
674    /// The compiled message used to execute the transaction.
675    ///
676    /// This can be used to map account indices in inner instructions back to
677    /// their corresponding pubkeys via `message.account_keys()`.
678    ///
679    /// This is `None` when the result is loaded from a fuzz fixture, since
680    /// fixtures don't contain the compiled message.
681    #[cfg(feature = "inner-instructions")]
682    pub message: Option<SanitizedMessage>,
683}
684
685impl MessageResult {
686    fn extract_ix_err(txn_err: TransactionError) -> InstructionError {
687        match txn_err {
688            TransactionError::InstructionError(_, ix_err) => ix_err,
689            _ => unreachable!(), // Mollusk only uses `InstructionError` variant.
690        }
691    }
692
693    fn extract_txn_program_result(
694        raw_result: &Result<(), TransactionError>,
695    ) -> TransactionProgramResult {
696        match raw_result {
697            Ok(()) => TransactionProgramResult::Success,
698            Err(TransactionError::InstructionError(idx, ix_err)) => {
699                let index = *idx as usize;
700                if let Ok(program_error) = ProgramError::try_from(ix_err.clone()) {
701                    TransactionProgramResult::Failure(index, program_error)
702                } else {
703                    TransactionProgramResult::UnknownError(index, ix_err.clone())
704                }
705            }
706            _ => unreachable!(), // Mollusk only uses `InstructionError` variant.
707        }
708    }
709}
710
711impl Mollusk {
712    fn new_inner(#[allow(unused)] enable_register_tracing: bool) -> Self {
713        #[rustfmt::skip]
714        solana_logger::setup_with_default(
715            "solana_rbpf::vm=debug,\
716             solana_runtime::message_processor=debug,\
717             solana_runtime::system_instruction_processor=trace",
718        );
719        let compute_budget = ComputeBudget::new_with_defaults(true, true);
720
721        #[cfg(feature = "fuzz")]
722        let feature_set = {
723            // Omit "test features" (they have the same u64 ID).
724            let mut fs = FeatureSet::all_enabled();
725            fs.active_mut()
726                .remove(&agave_feature_set::disable_sbpf_v0_execution::id());
727            fs.active_mut()
728                .remove(&agave_feature_set::reenable_sbpf_v0_execution::id());
729            fs
730        };
731        #[cfg(not(feature = "fuzz"))]
732        let feature_set = FeatureSet::all_enabled();
733
734        let program_cache =
735            ProgramCache::new(&feature_set, &compute_budget, enable_register_tracing);
736
737        #[allow(unused_mut)]
738        let mut me = Self {
739            config: Config::default(),
740            compute_budget,
741            epoch_stake: EpochStake::default(),
742            feature_set,
743            logger: None,
744            program_cache,
745            sysvars: Sysvars::default(),
746
747            #[cfg(feature = "invocation-inspect-callback")]
748            invocation_inspect_callback: Box::new(EmptyInvocationInspectCallback {}),
749
750            #[cfg(feature = "invocation-inspect-callback")]
751            enable_register_tracing,
752
753            #[cfg(feature = "fuzz-fd")]
754            slot: 0,
755        };
756
757        #[cfg(feature = "register-tracing")]
758        if enable_register_tracing {
759            // Have a default register tracing callback if register tracing is
760            // enabled.
761            me.invocation_inspect_callback = Box::new(DefaultRegisterTracingCallback::default());
762        }
763
764        me
765    }
766
767    /// Create a new Mollusk instance containing the provided program.
768    ///
769    /// Attempts to load the program's ELF file from the default search paths.
770    /// Once loaded, adds the program to the program cache and returns the
771    /// newly created Mollusk instance.
772    ///
773    /// # Default Search Paths
774    ///
775    /// The following locations are checked in order:
776    ///
777    /// - `tests/fixtures`
778    /// - The directory specified by the `BPF_OUT_DIR` environment variable
779    /// - The directory specified by the `SBF_OUT_DIR` environment variable
780    /// - The current working directory
781    pub fn new(program_id: &Pubkey, program_name: &str) -> Self {
782        let mut mollusk = Self::default();
783        mollusk.add_program(program_id, program_name);
784        mollusk
785    }
786
787    /// Create a new Mollusk instance with configurable debugging features.
788    ///
789    /// This constructor allows enabling low-level VM debugging capabilities,
790    /// such as register tracing, which are baked into program executables at
791    /// load time and cannot be changed afterwards.
792    ///
793    /// When `enable_register_tracing` is `true`:
794    /// - Programs are loaded with register tracing support
795    /// - A default [`DefaultRegisterTracingCallback`] is installed
796    /// - Trace data is written to `SBF_TRACE_DIR` (or `target/sbf/trace` by
797    ///   default)
798    #[cfg(feature = "register-tracing")]
799    pub fn new_debuggable(
800        program_id: &Pubkey,
801        program_name: &str,
802        enable_register_tracing: bool,
803    ) -> Self {
804        let mut mollusk = Self::new_inner(enable_register_tracing);
805        mollusk.add_program(program_id, program_name);
806        mollusk
807    }
808
809    /// Add a program to the test environment.
810    ///
811    /// If you intend to CPI to a program, this is likely what you want to use.
812    pub fn add_program(&mut self, program_id: &Pubkey, program_name: &str) {
813        self.add_program_with_loader(program_id, program_name, &DEFAULT_LOADER_KEY);
814    }
815
816    /// Add a program to the test environment under the specified loader.
817    ///
818    /// If you intend to CPI to a program, this is likely what you want to use.
819    pub fn add_program_with_loader(
820        &mut self,
821        program_id: &Pubkey,
822        program_name: &str,
823        loader_key: &Pubkey,
824    ) {
825        let elf = file::load_program_elf(program_name);
826        self.add_program_with_loader_and_elf(program_id, loader_key, &elf);
827    }
828
829    /// Add a program to the test environment using a provided ELF under a
830    /// specific loader.
831    ///
832    /// If you intend to CPI to a program, this is likely what you want to use.
833    pub fn add_program_with_loader_and_elf(
834        &mut self,
835        program_id: &Pubkey,
836        loader_key: &Pubkey,
837        elf: &[u8],
838    ) {
839        self.program_cache.add_program(program_id, loader_key, elf);
840    }
841
842    /// Warp the test environment to a slot by updating sysvars.
843    pub fn warp_to_slot(&mut self, slot: u64) {
844        self.sysvars.warp_to_slot(slot)
845    }
846
847    fn get_loader_key(&self, program_id: &Pubkey) -> Pubkey {
848        if crate::program::precompile_keys::is_precompile(program_id) {
849            crate::program::loader_keys::NATIVE_LOADER
850        } else {
851            self.program_cache
852                .load_program(program_id)
853                .or_panic_with(MolluskError::ProgramNotCached(program_id))
854                .account_owner()
855        }
856    }
857
858    // Determine the accounts to fallback to during account compilation.
859    fn get_account_fallbacks<'a>(
860        &self,
861        all_program_ids: impl Iterator<Item = &'a Pubkey>,
862        all_instructions: impl Iterator<Item = &'a Instruction>,
863        accounts: &[(Pubkey, Account)],
864    ) -> HashMap<Pubkey, Account> {
865        // Use a HashSet for fast lookups.
866        let account_keys: HashSet<&Pubkey> = accounts.iter().map(|(key, _)| key).collect();
867
868        let mut fallbacks = HashMap::new();
869
870        // Top-level target programs.
871        all_program_ids.for_each(|program_id| {
872            if !account_keys.contains(program_id) {
873                // Fallback to a stub.
874                fallbacks.insert(
875                    *program_id,
876                    Account {
877                        owner: self.get_loader_key(program_id),
878                        executable: true,
879                        ..Default::default()
880                    },
881                );
882            }
883        });
884
885        // Instructions sysvar.
886        if !account_keys.contains(&solana_instructions_sysvar::ID) {
887            // Fallback to the actual implementation of the sysvar.
888            let (ix_sysvar_id, ix_sysvar_acct) =
889                crate::instructions_sysvar::keyed_account(all_instructions);
890            fallbacks.insert(ix_sysvar_id, ix_sysvar_acct);
891        }
892
893        fallbacks
894    }
895
896    fn create_transaction_context(
897        &self,
898        transaction_accounts: Vec<(Pubkey, AccountSharedData)>,
899    ) -> TransactionContext<'_> {
900        TransactionContext::new(
901            transaction_accounts,
902            self.sysvars.rent.clone(),
903            self.compute_budget.max_instruction_stack_depth,
904            self.compute_budget.max_instruction_trace_length,
905        )
906    }
907
908    #[cfg(feature = "inner-instructions")]
909    fn deconstruct_inner_instructions(
910        transaction_context: &mut TransactionContext,
911    ) -> Vec<Vec<InnerInstruction>> {
912        let ix_trace = transaction_context.take_instruction_trace();
913        let mut all_inner_instructions: Vec<Vec<InnerInstruction>> = Vec::new();
914
915        for ix_in_trace in ix_trace {
916            let stack_height = ix_in_trace.nesting_level.saturating_add(1);
917
918            if stack_height == 1 {
919                // Top-level instruction: start a new empty group for its inner instructions.
920                all_inner_instructions.push(Vec::new());
921            } else if let Some(last_group) = all_inner_instructions.last_mut() {
922                // Inner instruction (CPI): add to the current group.
923                let inner_instruction = InnerInstruction {
924                    instruction: CompiledInstruction::new_from_raw_parts(
925                        ix_in_trace.program_account_index_in_tx as u8,
926                        ix_in_trace.instruction_data.to_vec(),
927                        ix_in_trace
928                            .instruction_accounts
929                            .iter()
930                            .map(|acc| acc.index_in_transaction as u8)
931                            .collect(),
932                    ),
933                    stack_height: u32::try_from(stack_height).ok(),
934                };
935                last_group.push(inner_instruction);
936            }
937        }
938
939        all_inner_instructions
940    }
941
942    fn deconstruct_resulting_accounts(
943        transaction_context: &TransactionContext,
944        original_accounts: &[(Pubkey, Account)],
945    ) -> Vec<(Pubkey, Account)> {
946        original_accounts
947            .iter()
948            .map(|(pubkey, account)| {
949                transaction_context
950                    .find_index_of_account(pubkey)
951                    .map(|index| {
952                        let account_ref = transaction_context.accounts().try_borrow(index).unwrap();
953                        let resulting_account = Account {
954                            lamports: account_ref.lamports(),
955                            data: account_ref.data().to_vec(),
956                            owner: *account_ref.owner(),
957                            executable: account_ref.executable(),
958                            rent_epoch: account_ref.rent_epoch(),
959                        };
960                        (*pubkey, resulting_account)
961                    })
962                    .unwrap_or((*pubkey, account.clone()))
963            })
964            .collect()
965    }
966
967    fn process_transaction_message<'a>(
968        &self,
969        sanitized_message: &'a SanitizedMessage,
970        transaction_context: &mut TransactionContext<'a>,
971        sysvar_cache: &SysvarCache,
972    ) -> MessageResult {
973        let mut compute_units_consumed = 0;
974        let mut timings = ExecuteTimings::default();
975
976        let mut program_cache = self.program_cache.cache();
977        let callback = MolluskInvokeContextCallback {
978            epoch_stake: &self.epoch_stake,
979            feature_set: &self.feature_set,
980        };
981        let execution_budget = self.compute_budget.to_budget();
982        let runtime_features = self.feature_set.runtime_features();
983
984        let _enable_register_tracing = false;
985        #[cfg(feature = "register-tracing")]
986        let _enable_register_tracing = self.enable_register_tracing;
987
988        let program_runtime_environments: ProgramRuntimeEnvironments = ProgramRuntimeEnvironments {
989            program_runtime_v1: Arc::new(
990                create_program_runtime_environment_v1(
991                    &runtime_features,
992                    &execution_budget,
993                    /* reject_deployment_of_broken_elfs */ false,
994                    /* debugging_features */ _enable_register_tracing,
995                )
996                .unwrap(),
997            ),
998            program_runtime_v2: Arc::new(create_program_runtime_environment_v2(
999                &execution_budget,
1000                /* debugging_features */ _enable_register_tracing,
1001            )),
1002        };
1003
1004        let mut invoke_context = InvokeContext::new(
1005            transaction_context,
1006            &mut program_cache,
1007            EnvironmentConfig::new(
1008                Hash::default(),
1009                /* blockhash_lamports_per_signature */ 5000, // The default value
1010                &callback,
1011                &runtime_features,
1012                &program_runtime_environments,
1013                &program_runtime_environments,
1014                sysvar_cache,
1015            ),
1016            self.logger.clone(),
1017            self.compute_budget.to_budget(),
1018            self.compute_budget.to_cost(),
1019        );
1020
1021        let mut raw_result = Ok(());
1022
1023        for (instruction_index, (program_id, compiled_ix)) in
1024            sanitized_message.program_instructions_iter().enumerate()
1025        {
1026            let program_id_index = compiled_ix.program_id_index as IndexOfAccount;
1027
1028            invoke_context
1029                .prepare_next_top_level_instruction(
1030                    sanitized_message,
1031                    &SVMInstruction::from(compiled_ix),
1032                    program_id_index,
1033                    &compiled_ix.data,
1034                )
1035                .expect("failed to prepare instruction");
1036
1037            #[cfg(feature = "invocation-inspect-callback")]
1038            {
1039                let instruction_context = invoke_context
1040                    .transaction_context
1041                    .get_next_instruction_context()
1042                    .unwrap();
1043                let instruction_accounts = instruction_context.instruction_accounts().to_vec();
1044                self.invocation_inspect_callback.before_invocation(
1045                    self,
1046                    program_id,
1047                    &compiled_ix.data,
1048                    &instruction_accounts,
1049                    &invoke_context,
1050                );
1051            }
1052
1053            let mut compute_units_consumed_instruction = 0u64;
1054            let invoke_result = if invoke_context.is_precompile(program_id) {
1055                invoke_context.process_precompile(
1056                    program_id,
1057                    &compiled_ix.data,
1058                    std::iter::once(compiled_ix.data.as_ref()),
1059                )
1060            } else {
1061                invoke_context
1062                    .process_instruction(&mut compute_units_consumed_instruction, &mut timings)
1063            };
1064            compute_units_consumed += compute_units_consumed_instruction;
1065
1066            #[cfg(feature = "invocation-inspect-callback")]
1067            self.invocation_inspect_callback.after_invocation(
1068                self,
1069                &invoke_context,
1070                self.enable_register_tracing,
1071            );
1072
1073            if let Err(err) = invoke_result {
1074                raw_result = Err(TransactionError::InstructionError(
1075                    instruction_index as u8,
1076                    err,
1077                ));
1078                break;
1079            }
1080        }
1081
1082        let return_data = transaction_context.get_return_data().1.to_vec();
1083
1084        #[cfg(feature = "inner-instructions")]
1085        let inner_instructions = Self::deconstruct_inner_instructions(transaction_context);
1086
1087        MessageResult {
1088            compute_units_consumed,
1089            execution_time: timings.details.execute_us.0,
1090            raw_result,
1091            return_data,
1092            #[cfg(feature = "inner-instructions")]
1093            inner_instructions,
1094            #[cfg(feature = "inner-instructions")]
1095            message: Some(sanitized_message.clone()),
1096        }
1097    }
1098
1099    fn process_instruction_chain_element(
1100        &self,
1101        index: usize,
1102        instruction: &Instruction,
1103        accounts: &[(Pubkey, Account)],
1104        fallback_accounts: &HashMap<Pubkey, Account>,
1105        sysvar_cache: &SysvarCache,
1106    ) -> InstructionResult {
1107        let (sanitized_message, transaction_accounts) = crate::compile_accounts::compile_accounts(
1108            std::slice::from_ref(instruction),
1109            accounts.iter(),
1110            fallback_accounts,
1111        );
1112
1113        let mut transaction_context = self.create_transaction_context(transaction_accounts);
1114        transaction_context.set_top_level_instruction_index(index);
1115
1116        let message_result = self.process_transaction_message(
1117            &sanitized_message,
1118            &mut transaction_context,
1119            sysvar_cache,
1120        );
1121
1122        let resulting_accounts = if message_result.raw_result.is_ok() {
1123            Self::deconstruct_resulting_accounts(&transaction_context, accounts)
1124        } else {
1125            accounts.to_vec()
1126        };
1127
1128        let raw_result = message_result
1129            .raw_result
1130            .map_err(MessageResult::extract_ix_err);
1131
1132        let this_result = InstructionResult {
1133            compute_units_consumed: message_result.compute_units_consumed,
1134            execution_time: message_result.execution_time,
1135            program_result: raw_result.clone().into(),
1136            raw_result,
1137            return_data: message_result.return_data,
1138            resulting_accounts,
1139            #[cfg(feature = "inner-instructions")]
1140            inner_instructions: message_result
1141                .inner_instructions
1142                .into_iter()
1143                .nth(index)
1144                .unwrap_or_default(),
1145            #[cfg(feature = "inner-instructions")]
1146            message: message_result.message,
1147        };
1148
1149        #[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
1150        fuzz::generate_fixtures_from_mollusk_test(self, instruction, accounts, &this_result);
1151
1152        this_result
1153    }
1154
1155    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1156    /// environment. Simply returns the result.
1157    ///
1158    /// For `fuzz` feature only:
1159    ///
1160    /// If the `EJECT_FUZZ_FIXTURES` environment variable is set, this function
1161    /// will convert the provided test to a fuzz fixture and write it to the
1162    /// provided directory.
1163    ///
1164    /// ```ignore
1165    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
1166    /// ```
1167    ///
1168    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
1169    /// JSON format.
1170    ///
1171    /// The `fuzz-fd` feature works the same way, but the variables require
1172    /// the `_FD` suffix, in case both features are active together
1173    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
1174    /// fixtures, which are structured a bit differently than Mollusk's own
1175    /// protobuf layouts.
1176    pub fn process_instruction(
1177        &self,
1178        instruction: &Instruction,
1179        accounts: &[(Pubkey, Account)],
1180    ) -> InstructionResult {
1181        let fallback_accounts = self.get_account_fallbacks(
1182            std::iter::once(&instruction.program_id),
1183            std::iter::once(instruction),
1184            accounts,
1185        );
1186
1187        let (sanitized_message, transaction_accounts) = crate::compile_accounts::compile_accounts(
1188            std::slice::from_ref(instruction),
1189            accounts.iter(),
1190            &fallback_accounts,
1191        );
1192
1193        let mut transaction_context = self.create_transaction_context(transaction_accounts);
1194        let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts);
1195
1196        let message_result = self.process_transaction_message(
1197            &sanitized_message,
1198            &mut transaction_context,
1199            &sysvar_cache,
1200        );
1201
1202        let resulting_accounts = if message_result.raw_result.is_ok() {
1203            Self::deconstruct_resulting_accounts(&transaction_context, accounts)
1204        } else {
1205            accounts.to_vec()
1206        };
1207
1208        let raw_result = message_result
1209            .raw_result
1210            .map_err(MessageResult::extract_ix_err);
1211
1212        let result = InstructionResult {
1213            compute_units_consumed: message_result.compute_units_consumed,
1214            execution_time: message_result.execution_time,
1215            program_result: raw_result.clone().into(),
1216            raw_result,
1217            return_data: message_result.return_data,
1218            resulting_accounts,
1219            #[cfg(feature = "inner-instructions")]
1220            inner_instructions: message_result
1221                .inner_instructions
1222                .into_iter()
1223                .next()
1224                .unwrap_or_default(),
1225            #[cfg(feature = "inner-instructions")]
1226            message: message_result.message,
1227        };
1228
1229        #[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
1230        fuzz::generate_fixtures_from_mollusk_test(self, instruction, accounts, &result);
1231
1232        result
1233    }
1234
1235    /// Process a chain of instructions using the minified Solana Virtual
1236    /// Machine (SVM) environment. The returned result is an
1237    /// `InstructionResult`, containing:
1238    ///
1239    /// * `compute_units_consumed`: The total compute units consumed across all
1240    ///   instructions.
1241    /// * `execution_time`: The total execution time across all instructions.
1242    /// * `program_result`: The program result of the _last_ instruction.
1243    /// * `resulting_accounts`: The resulting accounts after the _last_
1244    ///   instruction.
1245    ///
1246    /// For `fuzz` feature only:
1247    ///
1248    /// Similar to `process_instruction`, if the `EJECT_FUZZ_FIXTURES`
1249    /// environment variable is set, this function will convert the provided
1250    /// test to a set of fuzz fixtures - each of which corresponds to a single
1251    /// instruction in the chain - and write them to the provided directory.
1252    ///
1253    /// ```ignore
1254    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
1255    /// ```
1256    ///
1257    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
1258    /// JSON format.
1259    ///
1260    /// The `fuzz-fd` feature works the same way, but the variables require
1261    /// the `_FD` suffix, in case both features are active together
1262    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
1263    /// fixtures, which are structured a bit differently than Mollusk's own
1264    /// protobuf layouts.
1265    ///
1266    /// Note: Unlike `process_transaction_instructions`, this creates a new
1267    /// transaction context for each instruction, bypassing any
1268    /// transaction-level restrictions and treating each instruction in the
1269    /// chain as its own standalone invocation. However, account changes are
1270    /// persisted between invocations.
1271    pub fn process_instruction_chain(
1272        &self,
1273        instructions: &[Instruction],
1274        accounts: &[(Pubkey, Account)],
1275    ) -> InstructionResult {
1276        let mut composite_result = InstructionResult {
1277            resulting_accounts: accounts.to_vec(),
1278            ..Default::default()
1279        };
1280
1281        let fallback_accounts = self.get_account_fallbacks(
1282            instructions.iter().map(|ix| &ix.program_id),
1283            instructions.iter(),
1284            accounts,
1285        );
1286
1287        let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts);
1288
1289        for (index, instruction) in instructions.iter().enumerate() {
1290            let this_result = self.process_instruction_chain_element(
1291                index,
1292                instruction,
1293                &composite_result.resulting_accounts,
1294                &fallback_accounts,
1295                &sysvar_cache,
1296            );
1297
1298            composite_result.absorb(this_result);
1299
1300            if composite_result.program_result.is_err() {
1301                break;
1302            }
1303        }
1304
1305        composite_result
1306    }
1307
1308    /// Process multiple instructions using a single shared transaction context.
1309    ///
1310    /// This API is the closest Mollusk offers to a transaction. All
1311    /// instructions are processed in the same message using the same
1312    /// transaction context. The result is atomic, meaning resulting accounts
1313    /// only reflect the end state of the entire instruction set if all are
1314    /// successful. Upon any error, the execution is returned immediately.
1315    ///
1316    /// The returned result is a `TransactionResult`, containing:
1317    ///
1318    /// * `compute_units_consumed`: The total compute units consumed across all
1319    ///   instructions.
1320    /// * `execution_time`: The total execution time across all instructions.
1321    /// * `program_result`: The result code of the last program's execution and
1322    ///   its index.
1323    /// * `resulting_accounts`: The resulting accounts after all instructions.
1324    pub fn process_transaction_instructions(
1325        &self,
1326        instructions: &[Instruction],
1327        accounts: &[(Pubkey, Account)],
1328    ) -> TransactionResult {
1329        let fallback_accounts = self.get_account_fallbacks(
1330            instructions.iter().map(|ix| &ix.program_id),
1331            instructions.iter(),
1332            accounts,
1333        );
1334
1335        let (sanitized_message, transaction_accounts) = crate::compile_accounts::compile_accounts(
1336            instructions,
1337            accounts.iter(),
1338            &fallback_accounts,
1339        );
1340
1341        let mut transaction_context = self.create_transaction_context(transaction_accounts);
1342        let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts);
1343
1344        let message_result = self.process_transaction_message(
1345            &sanitized_message,
1346            &mut transaction_context,
1347            &sysvar_cache,
1348        );
1349
1350        let resulting_accounts = if message_result.raw_result.is_ok() {
1351            Self::deconstruct_resulting_accounts(&transaction_context, accounts)
1352        } else {
1353            accounts.to_vec()
1354        };
1355
1356        let program_result = MessageResult::extract_txn_program_result(&message_result.raw_result);
1357
1358        TransactionResult {
1359            compute_units_consumed: message_result.compute_units_consumed,
1360            execution_time: message_result.execution_time,
1361            program_result,
1362            raw_result: message_result.raw_result,
1363            return_data: message_result.return_data,
1364            resulting_accounts,
1365            #[cfg(feature = "inner-instructions")]
1366            inner_instructions: message_result.inner_instructions,
1367            #[cfg(feature = "inner-instructions")]
1368            message: message_result.message,
1369        }
1370    }
1371
1372    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1373    /// environment, then perform checks on the result. Panics if any checks
1374    /// fail.
1375    ///
1376    /// For `fuzz` feature only:
1377    ///
1378    /// If the `EJECT_FUZZ_FIXTURES` environment variable is set, this function
1379    /// will convert the provided test to a fuzz fixture and write it to the
1380    /// provided directory.
1381    ///
1382    /// ```ignore
1383    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
1384    /// ```
1385    ///
1386    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
1387    /// JSON format.
1388    ///
1389    /// The `fuzz-fd` feature works the same way, but the variables require
1390    /// the `_FD` suffix, in case both features are active together
1391    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
1392    /// fixtures, which are structured a bit differently than Mollusk's own
1393    /// protobuf layouts.
1394    pub fn process_and_validate_instruction(
1395        &self,
1396        instruction: &Instruction,
1397        accounts: &[(Pubkey, Account)],
1398        checks: &[Check],
1399    ) -> InstructionResult {
1400        let result = self.process_instruction(instruction, accounts);
1401        result.run_checks(checks, &self.config, self);
1402        result
1403    }
1404
1405    /// Process a chain of instructions using the minified Solana Virtual
1406    /// Machine (SVM) environment, then perform checks on the result.
1407    /// Panics if any checks fail.
1408    ///
1409    /// For `fuzz` feature only:
1410    ///
1411    /// Similar to `process_and_validate_instruction`, if the
1412    /// `EJECT_FUZZ_FIXTURES` environment variable is set, this function will
1413    /// convert the provided test to a set of fuzz fixtures - each of which
1414    /// corresponds to a single instruction in the chain - and write them to
1415    /// the provided directory.
1416    ///
1417    /// ```ignore
1418    /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
1419    /// ```
1420    ///
1421    /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
1422    /// JSON format.
1423    ///
1424    /// The `fuzz-fd` feature works the same way, but the variables require
1425    /// the `_FD` suffix, in case both features are active together
1426    /// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
1427    /// fixtures, which are structured a bit differently than Mollusk's own
1428    /// protobuf layouts.
1429    ///
1430    /// Note: Unlike `process_and_validate_transaction_instructions`, this
1431    /// creates a new transaction context for each instruction, bypassing any
1432    /// transaction-level restrictions and treating each instruction in the
1433    /// chain as its own standalone invocation. However, account changes are
1434    /// persisted between invocations.
1435    pub fn process_and_validate_instruction_chain(
1436        &self,
1437        instructions: &[(&Instruction, &[Check])],
1438        accounts: &[(Pubkey, Account)],
1439    ) -> InstructionResult {
1440        let mut composite_result = InstructionResult {
1441            resulting_accounts: accounts.to_vec(),
1442            ..Default::default()
1443        };
1444
1445        let fallback_accounts = self.get_account_fallbacks(
1446            instructions.iter().map(|(ix, _)| &ix.program_id),
1447            instructions.iter().map(|(ix, _)| *ix),
1448            accounts,
1449        );
1450
1451        let sysvar_cache = self.sysvars.setup_sysvar_cache(accounts);
1452
1453        for (index, (instruction, checks)) in instructions.iter().enumerate() {
1454            let this_result = self.process_instruction_chain_element(
1455                index,
1456                instruction,
1457                &composite_result.resulting_accounts,
1458                &fallback_accounts,
1459                &sysvar_cache,
1460            );
1461
1462            this_result.run_checks(checks, &self.config, self);
1463
1464            composite_result.absorb(this_result);
1465
1466            if composite_result.program_result.is_err() {
1467                break;
1468            }
1469        }
1470
1471        composite_result
1472    }
1473
1474    /// Process multiple instructions using a single shared transaction context,
1475    /// then perform checks on the result. Panics if any checks fail.
1476    ///
1477    /// This API is the closest Mollusk offers to a transaction. All
1478    /// instructions are processed in the same message using the same
1479    /// transaction context. The result is atomic, meaning resulting accounts
1480    /// only reflect the end state of the entire instruction set if all are
1481    /// successful. Upon any error, the execution is returned immediately.
1482    ///
1483    /// The returned result is a `TransactionResult`, containing:
1484    ///
1485    /// * `compute_units_consumed`: The total compute units consumed across all
1486    ///   instructions.
1487    /// * `execution_time`: The total execution time across all instructions.
1488    /// * `program_result`: The result code of the last program's execution and
1489    ///   its index.
1490    /// * `resulting_accounts`: The resulting accounts after all instructions.
1491    pub fn process_and_validate_transaction_instructions(
1492        &self,
1493        instructions: &[Instruction],
1494        accounts: &[(Pubkey, Account)],
1495        checks: &[Check],
1496    ) -> TransactionResult {
1497        let result = self.process_transaction_instructions(instructions, accounts);
1498        result.run_checks(checks, &self.config, self);
1499        result
1500    }
1501
1502    #[cfg(feature = "fuzz")]
1503    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
1504    /// environment.
1505    ///
1506    /// Fixtures provide an API to `decode` a raw blob, as well as read
1507    /// fixtures from files. Those fixtures can then be provided to this
1508    /// function to process them and get a Mollusk result.
1509    ///
1510    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1511    /// into the test environment will alter `Mollusk` values, such as compute
1512    /// budget and sysvars. However, the program cache remains unchanged.
1513    ///
1514    /// Therefore, developers can provision a `Mollusk` instance, set up their
1515    /// desired program cache, and then run a series of fixtures against that
1516    /// `Mollusk` instance (and cache).
1517    pub fn process_fixture(
1518        &mut self,
1519        fixture: &mollusk_svm_fuzz_fixture::Fixture,
1520    ) -> InstructionResult {
1521        let fuzz::mollusk::ParsedFixtureContext {
1522            accounts,
1523            compute_budget,
1524            feature_set,
1525            instruction,
1526            sysvars,
1527        } = fuzz::mollusk::parse_fixture_context(&fixture.input);
1528        self.compute_budget = compute_budget;
1529        self.feature_set = feature_set;
1530        self.sysvars = sysvars;
1531        self.process_instruction(&instruction, &accounts)
1532    }
1533
1534    #[cfg(feature = "fuzz")]
1535    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
1536    /// environment and compare the result against the fixture's effects.
1537    ///
1538    /// Fixtures provide an API to `decode` a raw blob, as well as read
1539    /// fixtures from files. Those fixtures can then be provided to this
1540    /// function to process them and get a Mollusk result.
1541    ///
1542    ///
1543    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1544    /// into the test environment will alter `Mollusk` values, such as compute
1545    /// budget and sysvars. However, the program cache remains unchanged.
1546    ///
1547    /// Therefore, developers can provision a `Mollusk` instance, set up their
1548    /// desired program cache, and then run a series of fixtures against that
1549    /// `Mollusk` instance (and cache).
1550    ///
1551    /// Note: To compare the result against the entire fixture effects, pass
1552    /// `&[FixtureCheck::All]` for `checks`.
1553    pub fn process_and_validate_fixture(
1554        &mut self,
1555        fixture: &mollusk_svm_fuzz_fixture::Fixture,
1556    ) -> InstructionResult {
1557        let result = self.process_fixture(fixture);
1558        InstructionResult::from(&fixture.output).compare_with_config(
1559            &result,
1560            &Compare::everything(),
1561            &self.config,
1562        );
1563        result
1564    }
1565
1566    #[cfg(feature = "fuzz")]
1567    /// a specific set of checks.
1568    ///
1569    /// This is useful for when you may not want to compare the entire effects,
1570    /// such as omitting comparisons of compute units consumed.
1571    /// Process a fuzz fixture using the minified Solana Virtual Machine (SVM)
1572    /// environment and compare the result against the fixture's effects using
1573    /// a specific set of checks.
1574    ///
1575    /// This is useful for when you may not want to compare the entire effects,
1576    /// such as omitting comparisons of compute units consumed.
1577    ///
1578    /// Fixtures provide an API to `decode` a raw blob, as well as read
1579    /// fixtures from files. Those fixtures can then be provided to this
1580    /// function to process them and get a Mollusk result.
1581    ///
1582    ///
1583    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1584    /// into the test environment will alter `Mollusk` values, such as compute
1585    /// budget and sysvars. However, the program cache remains unchanged.
1586    ///
1587    /// Therefore, developers can provision a `Mollusk` instance, set up their
1588    /// desired program cache, and then run a series of fixtures against that
1589    /// `Mollusk` instance (and cache).
1590    ///
1591    /// Note: To compare the result against the entire fixture effects, pass
1592    /// `&[FixtureCheck::All]` for `checks`.
1593    pub fn process_and_partially_validate_fixture(
1594        &mut self,
1595        fixture: &mollusk_svm_fuzz_fixture::Fixture,
1596        checks: &[Compare],
1597    ) -> InstructionResult {
1598        let result = self.process_fixture(fixture);
1599        let expected = InstructionResult::from(&fixture.output);
1600        result.compare_with_config(&expected, checks, &self.config);
1601        result
1602    }
1603
1604    #[cfg(feature = "fuzz-fd")]
1605    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1606    /// Machine (SVM) environment.
1607    ///
1608    /// Fixtures provide an API to `decode` a raw blob, as well as read
1609    /// fixtures from files. Those fixtures can then be provided to this
1610    /// function to process them and get a Mollusk result.
1611    ///
1612    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1613    /// into the test environment will alter `Mollusk` values, such as compute
1614    /// budget and sysvars. However, the program cache remains unchanged.
1615    ///
1616    /// Therefore, developers can provision a `Mollusk` instance, set up their
1617    /// desired program cache, and then run a series of fixtures against that
1618    /// `Mollusk` instance (and cache).
1619    pub fn process_firedancer_fixture(
1620        &mut self,
1621        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1622    ) -> InstructionResult {
1623        let fuzz::firedancer::ParsedFixtureContext {
1624            accounts,
1625            compute_budget,
1626            feature_set,
1627            instruction,
1628            slot,
1629        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1630        self.compute_budget = compute_budget;
1631        self.feature_set = feature_set;
1632        self.slot = slot;
1633        self.process_instruction(&instruction, &accounts)
1634    }
1635
1636    #[cfg(feature = "fuzz-fd")]
1637    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1638    /// Machine (SVM) environment and compare the result against the
1639    /// fixture's effects.
1640    ///
1641    /// Fixtures provide an API to `decode` a raw blob, as well as read
1642    /// fixtures from files. Those fixtures can then be provided to this
1643    /// function to process them and get a Mollusk result.
1644    ///
1645    ///
1646    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1647    /// into the test environment will alter `Mollusk` values, such as compute
1648    /// budget and sysvars. However, the program cache remains unchanged.
1649    ///
1650    /// Therefore, developers can provision a `Mollusk` instance, set up their
1651    /// desired program cache, and then run a series of fixtures against that
1652    /// `Mollusk` instance (and cache).
1653    ///
1654    /// Note: To compare the result against the entire fixture effects, pass
1655    /// `&[FixtureCheck::All]` for `checks`.
1656    pub fn process_and_validate_firedancer_fixture(
1657        &mut self,
1658        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1659    ) -> InstructionResult {
1660        let fuzz::firedancer::ParsedFixtureContext {
1661            accounts,
1662            compute_budget,
1663            feature_set,
1664            instruction,
1665            slot,
1666        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1667        self.compute_budget = compute_budget;
1668        self.feature_set = feature_set;
1669        self.slot = slot;
1670
1671        let result = self.process_instruction(&instruction, &accounts);
1672        let expected_result = fuzz::firedancer::parse_fixture_effects(
1673            &accounts,
1674            self.compute_budget.compute_unit_limit,
1675            &fixture.output,
1676        );
1677
1678        expected_result.compare_with_config(&result, &Compare::everything(), &self.config);
1679        result
1680    }
1681
1682    #[cfg(feature = "fuzz-fd")]
1683    /// Process a Firedancer fuzz fixture using the minified Solana Virtual
1684    /// Machine (SVM) environment and compare the result against the
1685    /// fixture's effects using a specific set of checks.
1686    ///
1687    /// This is useful for when you may not want to compare the entire effects,
1688    /// such as omitting comparisons of compute units consumed.
1689    ///
1690    /// Fixtures provide an API to `decode` a raw blob, as well as read
1691    /// fixtures from files. Those fixtures can then be provided to this
1692    /// function to process them and get a Mollusk result.
1693    ///
1694    ///
1695    /// Note: This is a mutable method on `Mollusk`, since loading a fixture
1696    /// into the test environment will alter `Mollusk` values, such as compute
1697    /// budget and sysvars. However, the program cache remains unchanged.
1698    ///
1699    /// Therefore, developers can provision a `Mollusk` instance, set up their
1700    /// desired program cache, and then run a series of fixtures against that
1701    /// `Mollusk` instance (and cache).
1702    ///
1703    /// Note: To compare the result against the entire fixture effects, pass
1704    /// `&[FixtureCheck::All]` for `checks`.
1705    pub fn process_and_partially_validate_firedancer_fixture(
1706        &mut self,
1707        fixture: &mollusk_svm_fuzz_fixture_firedancer::Fixture,
1708        checks: &[Compare],
1709    ) -> InstructionResult {
1710        let fuzz::firedancer::ParsedFixtureContext {
1711            accounts,
1712            compute_budget,
1713            feature_set,
1714            instruction,
1715            slot,
1716        } = fuzz::firedancer::parse_fixture_context(&fixture.input);
1717        self.compute_budget = compute_budget;
1718        self.feature_set = feature_set;
1719        self.slot = slot;
1720
1721        let result = self.process_instruction(&instruction, &accounts);
1722        let expected = fuzz::firedancer::parse_fixture_effects(
1723            &accounts,
1724            self.compute_budget.compute_unit_limit,
1725            &fixture.output,
1726        );
1727
1728        result.compare_with_config(&expected, checks, &self.config);
1729        result
1730    }
1731
1732    /// Convert this `Mollusk` instance into a `MolluskContext` for stateful
1733    /// testing.
1734    ///
1735    /// Creates a context wrapper that manages persistent state between
1736    /// instruction executions, starting with the provided account store.
1737    ///
1738    /// See [`MolluskContext`] for more details on how to use it.
1739    pub fn with_context<AS: AccountStore>(self, mut account_store: AS) -> MolluskContext<AS> {
1740        // For convenience, load all program accounts into the account store,
1741        // but only if they don't exist.
1742        self.program_cache
1743            .get_all_keyed_program_accounts()
1744            .into_iter()
1745            .for_each(|(pubkey, account)| {
1746                if account_store.get_account(&pubkey).is_none() {
1747                    account_store.store_account(pubkey, account);
1748                }
1749            });
1750        MolluskContext {
1751            mollusk: self,
1752            account_store: Rc::new(RefCell::new(account_store)),
1753            hydrate_store: true, // <-- Default
1754        }
1755    }
1756}
1757
1758/// A stateful wrapper around `Mollusk` that provides additional context and
1759/// convenience features for testing programs.
1760///
1761/// `MolluskContext` maintains persistent state between instruction executions,
1762/// starting with an account store that automatically manages account
1763/// lifecycles. This makes it ideal for complex testing scenarios involving
1764/// multiple instructions, instruction chains, and stateful program
1765/// interactions.
1766///
1767/// Note: Account state is only persisted if the instruction execution
1768/// was successful. If an instruction fails, the account state will not
1769/// be updated.
1770///
1771/// The API is functionally identical to `Mollusk` but with enhanced state
1772/// management and a streamlined interface. Namely, the input `accounts` slice
1773/// is no longer required, and the returned result does not contain a
1774/// `resulting_accounts` field.
1775pub struct MolluskContext<AS: AccountStore> {
1776    pub mollusk: Mollusk,
1777    pub account_store: Rc<RefCell<AS>>,
1778    pub hydrate_store: bool,
1779}
1780
1781impl<AS: AccountStore> MolluskContext<AS> {
1782    fn load_accounts_for_instructions<'a>(
1783        &self,
1784        instructions: impl Iterator<Item = &'a Instruction>,
1785    ) -> Vec<(Pubkey, Account)> {
1786        let mut accounts = Vec::new();
1787
1788        // If hydration is enabled, add sysvars and program accounts regardless
1789        // of whether or not they exist already.
1790        if self.hydrate_store {
1791            self.mollusk
1792                .program_cache
1793                .get_all_keyed_program_accounts()
1794                .into_iter()
1795                .chain(self.mollusk.sysvars.get_all_keyed_sysvar_accounts())
1796                .for_each(|(pubkey, account)| {
1797                    accounts.push((pubkey, account));
1798                });
1799        }
1800
1801        // Regardless of hydration, only add an account if the caller hasn't
1802        // already loaded it into the store.
1803        let mut seen = HashSet::new();
1804        let store = self.account_store.borrow();
1805        instructions.for_each(|instruction| {
1806            instruction
1807                .accounts
1808                .iter()
1809                .for_each(|AccountMeta { pubkey, .. }| {
1810                    if seen.insert(*pubkey) && pubkey != &solana_instructions_sysvar::id() {
1811                        // First try to load theirs, then see if it's a sysvar,
1812                        // then see if it's a cached program, then apply the
1813                        // default.
1814                        let account = store.get_account(pubkey).unwrap_or_else(|| {
1815                            self.mollusk
1816                                .sysvars
1817                                .maybe_create_sysvar_account(pubkey)
1818                                .unwrap_or_else(|| {
1819                                    self.mollusk
1820                                        .program_cache
1821                                        .maybe_create_program_account(pubkey)
1822                                        .unwrap_or_else(|| store.default_account(pubkey))
1823                                })
1824                        });
1825                        accounts.push((*pubkey, account));
1826                    }
1827                });
1828        });
1829        accounts
1830    }
1831
1832    fn consume_mollusk_result(&self, result: &InstructionResult) {
1833        if result.program_result.is_ok() {
1834            // Only store resulting accounts if the result was success.
1835            let mut store = self.account_store.borrow_mut();
1836            for (pubkey, account) in result.resulting_accounts.iter() {
1837                store.store_account(*pubkey, account.clone());
1838            }
1839        }
1840    }
1841
1842    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1843    /// environment. Simply returns the result.
1844    pub fn process_instruction(&self, instruction: &Instruction) -> InstructionResult {
1845        let accounts = self.load_accounts_for_instructions(once(instruction));
1846        let result = self.mollusk.process_instruction(instruction, &accounts);
1847        self.consume_mollusk_result(&result);
1848        result
1849    }
1850
1851    /// Process a chain of instructions using the minified Solana Virtual
1852    /// Machine (SVM) environment.
1853    pub fn process_instruction_chain(&self, instructions: &[Instruction]) -> InstructionResult {
1854        let accounts = self.load_accounts_for_instructions(instructions.iter());
1855        let result = self
1856            .mollusk
1857            .process_instruction_chain(instructions, &accounts);
1858        self.consume_mollusk_result(&result);
1859        result
1860    }
1861
1862    /// Process an instruction using the minified Solana Virtual Machine (SVM)
1863    /// environment, then perform checks on the result.
1864    pub fn process_and_validate_instruction(
1865        &self,
1866        instruction: &Instruction,
1867        checks: &[Check],
1868    ) -> InstructionResult {
1869        let accounts = self.load_accounts_for_instructions(once(instruction));
1870        let result = self
1871            .mollusk
1872            .process_and_validate_instruction(instruction, &accounts, checks);
1873        self.consume_mollusk_result(&result);
1874        result
1875    }
1876
1877    /// Process a chain of instructions using the minified Solana Virtual
1878    /// Machine (SVM) environment, then perform checks on the result.
1879    pub fn process_and_validate_instruction_chain(
1880        &self,
1881        instructions: &[(&Instruction, &[Check])],
1882    ) -> InstructionResult {
1883        let accounts = self.load_accounts_for_instructions(
1884            instructions.iter().map(|(instruction, _)| *instruction),
1885        );
1886        let result = self
1887            .mollusk
1888            .process_and_validate_instruction_chain(instructions, &accounts);
1889        self.consume_mollusk_result(&result);
1890        result
1891    }
1892}