Mollusk

repository·main·Indexed 43 days ago

https://github.com/anza-xyz/mollusk

A high-performance, lightweight test harness for Solana programs that executes instructions in a minified Solana Virtual Machine (SVM) environment. It avoids the overhead of a full validator by provisioning a program execution pipeline directly from lower-level SVM components. Mollusk includes tools for stateful testing via MolluskContext, compute unit benchmarking with MolluskComputeUnitBencher and MolluskComputeUnitMatrixBencher, fixture generation, CPI tracking, and register tracing for debugging.

Tokens
31.2K
Snippets
98
Records
120
Agent score
54%

What's inside Mollusk

  1. What is Mollusk?

    main

    Mollusk is a lightweight test harness for Solana programs designed for high-speed testing in a minified Solana Virtual Machine (SVM) environment.

    Unlike a full validator runtime, Mollusk provisions a program execution pipeline directly from lower-level SVM components (like the BPF Loader). It avoids heavy components like AccountsDB or Bank, making it exceptionally fast.

    Key Trade-off: Because it lacks a full account database, you must provide an explicit list of accounts to use for every execution.

  2. Perform stateful testing with MolluskContext

    main

    For complex scenarios where account state must persist between multiple instruction calls, use MolluskContext.

    MolluskContext wraps a Mollusk instance and an AccountStore. It automatically manages accounts: when an instruction executes, the resulting account states are automatically stored in the context. This eliminates the need to manually pass the full account list for every subsequent instruction.

    To use it, call mollusk.with_context(account_store) where account_store implements the AccountStore trait. Note that HashMap<Pubkey, Account> implements AccountStore directly for easy use.

    use {
        mollusk_svm::{Mollusk, account_store::AccountStore},
        solana_account::Account,
        solana_instruction::Instruction,
        solana_pubkey::Pubkey,
        solana_system_interface::instruction as system_instruction,
        std::collections::HashMap,
    };
    
    // Simple in-memory account store implementation
    #[derive(Default)]
    struct InMemoryAccountStore {
        accounts: HashMap<Pubkey, Account>,
    }
    
    impl AccountStore for InMemoryAccountStore {
        fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
            self.accounts.get(pubkey).cloned()
        }
    
        fn store_account(&mut self, pubkey: Pubkey, account: Account) {
            self.accounts.insert(pubkey, account);
        }
    }
    
    let mollusk = Mollusk::default();
    let context = mollusk.with_context(InMemoryAccountStore::default());
    
    let alice = Pubkey::new_unique();
    let bob = Pubkey::new_unique();
    
    // Execute instructions without managing accounts manually
    let instruction1 = system_instruction::transfer(&alice, &bob, 1_000_000);
    let result1 = context.process_instruction(&instruction1);
    
    let instruction2 = system_instruction::transfer(&bob, &alice, 500_000);
    let result2 = context.process_instruction(&instruction2);
    
    // Account state is automatically preserved between calls
  3. Compare multiple program implementations with Matrix Benchmarking

    main

    If you need to compare different versions of a program (e.g., an optimized version vs. a baseline), use MolluskComputeUnitMatrixBencher. This generates a markdown table where each program implementation is represented as a column, allowing for direct CU comparison across versions.

    use {
        mollusk_svm_bencher::MolluskComputeUnitMatrixBencher,
        mollusk_svm::Mollusk,
        /* ... */
    };
    
    /* Instruction & accounts setup ... */
    
    let mollusk = Mollusk::new(&program_id, "program_v1");
    
    MolluskComputeUnitMatrixBencher::new(mollusk)
        .programs(&["program_v1", "program_v2", "program_v3"])
        .bench(("bench0", &instruction0, &accounts0))
        .bench(("bench1", &instruction1, &accounts1))
        .must_pass(true)
        .out_dir("../target/benches")
        .execute();
  4. Benchmark Compute Unit usage with MolluskComputeUnitBencher

    main

    Use MolluskComputeUnitBencher to measure the compute unit (CU) consumption of Solana programs. It allows you to define multiple benchmarks and generates a markdown report. If a benchmark has a previous value, the report includes the delta, helping you track the impact of code changes on CU usage.

    To run these benchmarks via cargo bench, ensure your Cargo.toml includes a benchmark definition:

    [[bench]]
    name = "compute_units"
    harness = false
    use {
        mollusk_svm_bencher::MolluskComputeUnitBencher,
        mollusk_svm::Mollusk,
        /* ... */
    };
    
    // Optionally disable logging.
    solana_logger::setup_with("");
    
    /* Instruction & accounts setup ... */
    
    let mollusk = Mollusk::new(&program_id, "my_program");
    
    MolluskComputeUnitBencher::new(mollusk)
        .bench(("bench0", &instruction0, &accounts0))
        .bench(("bench1", &instruction1, &accounts1))
        .bench(("bench2", &instruction2, &accounts2))
        .bench(("bench3", &instruction3, &accounts3))
        .must_pass(true)
        .out_dir("../target/benches")
        .execute();
  5. Track inner instructions (CPIs)

    main

    Mollusk can track cross-program invocations (CPIs) when the inner-instructions feature is enabled. This captures the program ID, instruction data, accounts, and the stack height for every CPI made during execution.

    1. Enable the feature in your Cargo.toml:

      [dependencies]
      mollusk-svm = { version = "0.8.0", features = ["inner-instructions"] }
    2. Validate CPIs using Check::inner_instruction_count(n) to ensure exactly n CPIs occurred.

    use {
        mollusk_svm::{Mollusk, result::Check},
        solana_sdk::{
            account::Account,
            instruction::{AccountMeta, Instruction},
            pubkey::Pubkey,
        },
    };
    
    let program_id = Pubkey::new_unique();
    let cpi_target_id = Pubkey::new_unique();
    
    let mut mollusk = Mollusk::new(&program_id, "my_program");
    mollusk.add_program(&cpi_target_id, "cpi_target_program");
    
    /* Setup instruction that performs a CPI ... */
    
    mollusk.process_and_validate_instruction(
        &instruction,
        &accounts,
        &[
            Check::success(),
            Check::inner_instruction_count(1), // Verify exactly 1 CPI occurred
        ],
    );
  6. Generate fixtures from Mollusk tests

    main

    Mollusk can automatically generate test fixtures by serializing process_instruction calls. This is useful for capturing a known-good state from one program version to use as a test suite for another.

    To generate fixtures, set the following environment variables during your test run:

    • EJECT_FUZZ_FIXTURES: Specifies the directory where fixtures will be saved (e.g., EJECT_FUZZ_FIXTURES="./fuzz-fixtures").
    • EJECT_FUZZ_FIXTURES_JSON: If set, fixtures will be written in JSON format.

    Example command:

    EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...

    Note: Fixture support is gated behind the fuzz or fuzz-fd feature flags.

  7. Load and execute fixtures

    main

    Fixtures are structured representations of test cases (input data, expected output, and context). You can load them from files or raw blobs and execute them using Mollusk.

    Key methods:

    • process_fixture: Executes the fixture and returns the result.
    • process_and_validate_fixture: Executes the fixture and compares the result against the fixture's effects.
    • process_and_partially_validate_fixture: Executes the fixture and compares the result against a specific subset of effects (e.g., ignoring compute units).

    To use these, you must enable the fuzz or fuzz-fd feature flags.

    use {
        mollusk_svm::{Mollusk, fuzz::check::FixtureCheck},
        solana_sdk::{account::Account, pubkey::Pubkey},
        std::{fs, path::Path},
    };
    
    let mollusk = Mollusk::default();
    
    for file in fs::read_dir(Path::new("fixtures-dir"))? {
        let fixture = Fixture::load_from_blob_file(&entry?.file_name());
    
        // Execute the fixture and apply partial checks.
        mollusk.process_and_partially_validate_fixture(
           &fixture,
           &[
               FixtureCheck::ProgramResult,
               FixtureCheck::ReturnData,
               FixtureCheck::all_resulting_accounts(),
           ],
        );
    }
  8. Enable register tracing and debugging

    main

    Mollusk supports register tracing and GDB remote debugging via the register-tracing and invocation-inspect-callback feature flags.

    Register Tracing

    To enable tracing without code changes, set the SBF_TRACE_DIR environment variable. Mollusk will then persist register sets and SBPF instructions to that directory.

    • SBF_TRACE_DIR: Directory for trace files.
    • SBF_TRACE_DISASSEMBLE: If set, produces a disassembled register trace.

    GDB Debugging

    If the sbpf-debugger feature is enabled and you set the SBF_DEBUG_PORT environment variable, the VM starts a GDB remote stub on that TCP port. You can then connect a debugger to inspect registers, memory, and step through SBPF execution.

    Programmatic Control

    You can toggle tracing on/off during a test run by swapping the invocation_inspect_callback between EmptyInvocationInspectCallback and register_tracing::DefaultRegisterTracingCallback.

    use mollusk_svm::{register_tracing, EmptyInvocationInspectCallback, Mollusk};
    
    assert!(std::env::var("SBF_TRACE_DIR").is_ok());
    
    /* Mollusk setup .. */
    /* Load programs .. */
    /* .. */
    /* Have tracing here. */
    
    /* Disable trace collection here. */
    mollusk.invocation_inspect_callback = Box::new(EmptyInvocationInspectCallback {});
    /* .. */
    
    /* Re-enable trace collection. */
    mollusk.invocation_inspect_callback =
        Box::new(register_tracing::DefaultRegisterTracingCallback {
            sbf_trace_dir: std::env::var("SBF_TRACE_DIR").unwrap(),
            sbf_trace_disassemble: std::env::var("SBF_TRACE_DISASSEMBLE").is_ok(),
            sbf_debug_port: std::env::var("SBF_DEBUG_PORT")
                .map(|port| port.parse::<u16>().ok())
                .unwrap_or_default(),
        });
  9. Use mollusk_svm_result for SVM execution validation

    main

    The mollusk_svm_result crate provides tools to validate and compare the outcomes of SVM (Solana Virtual Machine) program executions. It allows you to define specific requirements for an execution (e.g., success status, compute unit usage, or account state changes) and verify if an InstructionResult meets those requirements.

    Core components include:

    • InstructionResult: The primary type containing execution details.
    • Check: A builder-style type used to define validation rules.
    • Compare: A trait for comparing two different instruction results.
    • Config: Configuration settings that govern how validation is performed.
    use mollusk_svm_result::{Check, Config, InstructionResult};
    
    let result = InstructionResult::default();
    let checks = vec![Check::success(), Check::compute_units(100)];
    let config = Config::default();
    
    // Note: 'mollusk' context must be provided to run_checks
    result.run_checks(&checks, &config, &mollusk);
  10. Customize validation context with CheckContext

    main

    The CheckContext trait allows developers to provide custom logic for validation checks when running them on standalone results.

    This is particularly useful if you need to customize how account properties are evaluated, such as using a custom Rent configuration to determine if an account is rent-exempt. By implementing this trait, you can override the default is_rent_exempt behavior used during the validation process.

    use mollusk_svm_result::CheckContext;
    use solana_pubkey::Pubkey;
    
    struct MyCustomContext;
    
    impl CheckContext for MyCustomContext {
        fn is_rent_exempt(&self, lamports: u64, space: usize, owner: Pubkey) -> bool {
            // Custom logic for rent exemption
            lamports > 1000
        }
    }
  11. Understand the SBF register tracing output files

    main

    When register tracing is enabled via DefaultRegisterTracingCallback, the callback persists several files to the configured sbf_trace_dir. Files are named using a prefix derived from the SHA-256 hash of the register trace.

    For a given trace, the following files are generated:

    • <hash_prefix>.regs: The raw register traces.
    • <hash_prefix>.insns: The instructions corresponding to the program counter (stored in r11) at each trace point.
    • <hash_prefix>.program_id: The Pubkey of the program being traced.
    • <hash_prefix>.trace: (Only if SBF_TRACE_DISASSEMBLE is enabled) A full disassembly of the register trace.
    • <hash_prefix>.exec.sha256: (If the program ELF is in the cache) The SHA-256 hash of the executable's bytes.
    • program_ids.map: A mapping of Pubkey=SHA256 for the top-level program and all instruction accounts that have ELF bytes in the cache. This is used by debugger clients to resolve symbols.
  12. Convert types into serializable fixtures using IntoSerializableFixture

    main
    The IntoSerializableFixture trait allows you to define a conversion from a high-level or temporary type into a formal SerializableFixture. This is used by FsHandler::new to initialize a handler with a type that is not yet in its final serializable form.