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}