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}