Skip to content

Feature Request: Adopt Mollusk's Validation System and Low-Level Control Features #198

@brimigs

Description

@brimigs

Summary

Mollusk is test harness for solana programs (https://github.com/anza-xyz/mollusk) that offers a lot of great functionality. I would love to see LiteSVM offer something similar.

Motivation

While liteSVM excels at transaction simulation and state management, Mollusk offers superior features for:

  • Precise validation through its Check system
  • Low-level control via custom syscalls
  • Compute unit benchmarking with dedicated tools
  • Instruction-level testing without transaction overhead

Adding these capabilities would make liteSVM a complete testing solution covering both unit and integration testing needs.

Proposed Features

1. Check-Based Validation System

Current State (liteSVM)

// Manual assertions after transaction
let result = svm.send_transaction(tx);
assert!(result.is_ok());

let account = svm.get_account(&counter).unwrap();
assert_eq!(account.lamports, expected_lamports);
assert_eq!(account.data[8], expected_value);
assert_eq!(account.owner, expected_owner);

Proposed API (Inspired by Mollusk)

// Declarative validation with rich checks
svm.send_and_validate_transaction(
    tx,
    &[
        Check::success(),
        Check::compute_units(450),
        Check::account(&counter)
            .lamports(expected_lamports)
            .data_slice(8, &[expected_value])
            .owner(&program_id)
            .rent_exempt()
            .build(),
        Check::logs_contain("Program log: Counter incremented"),
    ],
);

Implementation Details

pub enum Check {
    /// Validate transaction succeeded
    Success,
    
    /// Validate transaction failed with specific error
    Err(TransactionError),
    
    /// Validate compute units consumed
    ComputeUnits(u64),
    
    /// Validate execution time
    ExecutionTime(Duration),
    
    /// Validate specific account state
    Account(AccountCheck),
    
    /// Validate all accounts are rent exempt
    AllRentExempt,
    
    /// Validate transaction logs
    LogsContain(&str),
    
    /// Validate return data
    ReturnData(&[u8]),
    
    /// Custom validation function
    Custom(Box<dyn Fn(&TransactionResult) -> bool>),
}

pub struct AccountCheck {
    pubkey: Pubkey,
    lamports: Option<u64>,
    data: Option<Vec<u8>>,
    data_slice: Option<(usize, Vec<u8>)>,
    owner: Option<Pubkey>,
    executable: Option<bool>,
    rent_exempt: Option<bool>,
    closed: bool,
}

impl AccountCheck {
    pub fn new(pubkey: &Pubkey) -> Self { ... }
    
    pub fn lamports(mut self, lamports: u64) -> Self {
        self.lamports = Some(lamports);
        self
    }
    
    pub fn data(mut self, data: &[u8]) -> Self {
        self.data = Some(data.to_vec());
        self
    }
    
    pub fn data_slice(mut self, offset: usize, data: &[u8]) -> Self {
        self.data_slice = Some((offset, data.to_vec()));
        self
    }
    
    pub fn closed(mut self) -> Self {
        self.closed = true;
        self
    }
    
    pub fn build(self) -> Check {
        Check::Account(self)
    }
}

2. Custom Syscall Support

Current State (liteSVM)

// No support for custom syscalls - cannot test programs that use them

Proposed API

use litesvm::syscall::{SyscallRegistry, CustomSyscall};

// Define custom syscall
struct CheckSpreadSyscall;

impl CustomSyscall for CheckSpreadSyscall {
    fn call(
        &self,
        invoke_context: &mut InvokeContext,
        args: &[u64],
        memory_mapping: &mut MemoryMapping,
    ) -> Result<u64, Error> {
        // Custom implementation
        let spread = calculate_spread();
        
        // Write result to memory
        let ptr = memory_mapping.map(AccessType::Store, args[0], 8)?;
        unsafe {
            std::ptr::write(ptr as *mut u64, spread);
        }
        
        Ok(0)
    }
}

// Register with liteSVM
let mut svm = LiteSVM::new();
svm.register_syscall("sol_check_spread", Box::new(CheckSpreadSyscall))?;

// Now programs using this syscall will work
let result = svm.send_transaction(tx);

Use Cases

  • Testing programs with custom syscalls
  • Mocking external dependencies
  • Simulating cross-program calls
  • Testing error conditions

3. Instruction-Level Processing

Current State (liteSVM)

// Must create full transaction even for single instruction
let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer), &[&payer], blockhash);
let result = svm.send_transaction(tx);

Proposed API

// Process single instruction without transaction overhead
let result = svm.process_instruction(
    &instruction,
    &[
        (account1_pubkey, account1),
        (account2_pubkey, account2),
    ],
);

// Process instruction chain
let results = svm.process_instruction_chain(
    &[instruction1, instruction2, instruction3],
    &accounts,
);

// With validation
svm.process_and_validate_instruction(
    &instruction,
    &accounts,
    &[Check::success(), Check::compute_units(300)],
);

4. Compute Unit Benchmarking

Proposed API

use litesvm::bencher::ComputeUnitBencher;

// Benchmark individual instructions
let bencher = ComputeUnitBencher::new(svm);

bencher
    .bench("initialize", &init_instruction, &init_accounts)
    .bench("increment", &increment_instruction, &increment_accounts)
    .bench("decrement", &decrement_instruction, &decrement_accounts)
    .must_pass(true)
    .output_markdown("benches/compute_units.md")
    .execute();

// Output markdown table:
// | Operation  | CUs   | Delta |
// |------------|-------|-------|
// | initialize | 450   | --    |
// | increment  | 372   | -78   |
// | decrement  | 372   | 0     |

5. Fixture Support

Proposed API

// Generate fixtures from tests
std::env::set_var("LITESVM_DUMP_FIXTURES", "./fixtures");
let result = svm.send_transaction(tx); // Automatically saves fixture

// Load and execute fixtures
let fixture = Fixture::load("./fixtures/test1.json")?;
let result = svm.process_fixture(&fixture);

// Validate against expected fixture output
svm.process_and_validate_fixture(
    &fixture,
    &[
        FixtureCheck::ProgramResult,
        FixtureCheck::ReturnData,
        FixtureCheck::AccountChanges,
    ],
);

6. Enhanced State Management Options

Proposed API

// Stateless mode (like Mollusk)
let svm = LiteSVM::stateless();
let result = svm.process_instruction(&ix, &accounts);
// State changes don't persist

// Explicit state management
let mut state = AccountStore::new();
state.insert(pubkey1, account1);

let svm = LiteSVM::with_state(state);
let result = svm.process_instruction(&ix);
// Access modified state
let modified_account = svm.state.get(&pubkey1);

Implementation Plan

Phase 1: Validation System (High Priority)

  • Implement Check enum and validation logic
  • Add AccountCheck builder
  • Create send_and_validate_transaction method
  • Add validation for logs and return data

Phase 2: Instruction Processing (High Priority)

  • Add process_instruction method
  • Implement process_instruction_chain
  • Add validation variants for instruction processing
  • Optimize for performance without transaction overhead

Phase 3: Custom Syscalls (Medium Priority)

  • Design syscall registry system
  • Implement CustomSyscall trait
  • Add memory mapping utilities
  • Create examples for common syscalls

Phase 4: Benchmarking Tools (Medium Priority)

  • Create ComputeUnitBencher struct
  • Implement markdown output
  • Add delta tracking between runs
  • Create CLI tool for benchmarking

Phase 5: Advanced Features (Low Priority)

  • Fixture generation and loading
  • Stateless mode option
  • Cross-implementation fixture format
  • Integration with fuzzing tools

Benefits

For Unit Testing

// Before: 20+ lines with manual assertions
// After: 5 lines with declarative validation
svm.process_and_validate_instruction(
    &instruction,
    &accounts,
    &[Check::success(), Check::account(&pubkey).lamports(1000).build()],
);

For Performance Testing

// Automatic compute unit tracking and regression detection
bencher.bench("critical_operation", &ix, &accounts);
// Generates report showing performance changes

For Complex Programs

// Test programs with custom syscalls
svm.register_syscall("sol_custom_op", CustomOp::new());
// Previously untestable programs now work

Comparison with Current State

Feature Current liteSVM With Proposed Changes Mollusk
Validation API Manual assertions Rich Check system ✓ Check system
Custom Syscalls ❌ Not supported ✓ Full support ✓ Full support
Instruction Processing Via transactions only ✓ Direct processing ✓ Direct processing
Compute Benchmarking Manual ✓ Built-in bencher ✓ Built-in bencher
Fixtures ❌ Not supported ✓ Generate/load ✓ Generate/load
State Management Always stateful ✓ Configurable Stateless by default

Backwards Compatibility

All proposed features would be additive:

  • Existing send_transaction API remains unchanged
  • New validation methods are optional
  • Instruction processing is additional API
  • Custom syscalls are opt-in

Performance Considerations

  • Instruction processing would be 30-50% faster than transaction processing
  • Validation checks add minimal overhead (< 5%)
  • Custom syscalls have same performance as native syscalls
  • Stateless mode reduces memory usage for unit tests

Use Case Examples

Example 1: Testing Compute-Intensive Operations

let bencher = ComputeUnitBencher::new(svm);
for size in [10, 100, 1000, 10000] {
    let (ix, accounts) = create_sort_instruction(size);
    bencher.bench(&format!("sort_{}", size), &ix, &accounts);
}
bencher.output_markdown("benches/sort_performance.md");

Example 2: Testing Custom Syscalls

// Program uses custom syscall for randomness
svm.register_syscall("sol_get_random", RandomnessSyscall::new(seed));

// Test different random scenarios
for seed in test_seeds {
    svm.update_syscall("sol_get_random", RandomnessSyscall::new(seed));
    let result = svm.process_instruction(&ix, &accounts);
    validate_random_behavior(result, seed);
}

Example 3: Precise State Validation

svm.process_and_validate_instruction(
    &complex_instruction,
    &accounts,
    &[
        Check::success(),
        Check::compute_units(5000),
        Check::account(&vault)
            .lamports(1_000_000)
            .data_slice(0, &[1, 2, 3, 4]) // Check header
            .data_slice(100, &[5, 6, 7, 8]) // Check specific field
            .owner(&program_id)
            .rent_exempt()
            .build(),
        Check::logs_contain("Vault updated successfully"),
        Check::return_data(&[0, 0, 0, 1]), // Success code
    ],
);

Success Metrics

  1. API Completeness: Feature parity with Mollusk for unit testing
  2. Performance: Instruction processing 30-50% faster than transactions
  3. Adoption: Used for both unit and integration tests
  4. Developer Satisfaction: Reduced test verbosity by 40%

Questions for Maintainers

  1. Should validation be a separate crate (litesvm-check) or built-in?
  2. Is there interest in maintaining fixture compatibility with Mollusk/Firedancer?
  3. Should custom syscalls use the same API as Agave or simplified version?
  4. Would you prefer trait-based or enum-based validation checks?

Conclusion

These features would give liteSVM users the best of both worlds: liteSVM's excellent transaction simulation and state management combined with Mollusk's precise validation and low-level control. This would make liteSVM the only testing framework needed for Solana development, suitable for everything from unit tests to complex integration scenarios.

The proposed changes maintain liteSVM's ease of use while adding optional power features for advanced users. Developers could start with simple transaction testing and gradually adopt more sophisticated features as needed.


Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions