Skip to content

igneous-labs/jiminy

Repository files navigation

jiminy

Yet another no-std no-dependencies library for writing solana programs.

Hello World

A simple jiminy program that CPIs the system program to transfer 1 SOL from the first input account to the second input account:

use jiminy_cpi::{program_error::BuiltInProgramError, Cpi};
use jiminy_entrypoint::program_error::ProgramError;
use jiminy_system_prog_interface::{TransferIxAccs, TransferIxData};

// Determines the maximum number of accounts that can be deserialized and
// saved to [`Accounts`]. Any proceeding accounts are discarded.
const MAX_ACCS: usize = 3;

// Determines the maximum number of accounts that can be used in a CPI,
// excluding the program being invoked.
const MAX_CPI_ACCS: usize = 2;

const ONE_SOL_IN_LAMPORTS: u64 = 1_000_000_000;

type Accounts<'account> = jiminy_entrypoint::account::Accounts<'account, MAX_ACCS>;

jiminy_entrypoint::entrypoint!(process_ix, MAX_ACCS);

fn process_ix(
    accounts: &mut Accounts,
    _data: &[u8],
    _prog_id: &[u8; 32],
) -> Result<(), ProgramError> {
    let (sys_prog, transfer_accs) = match accounts.as_slice().split_last_chunk() {
        Some((&[sys_prog], ta)) => (sys_prog, TransferIxAccs(*ta)),
        _ => {
            return Err(ProgramError::from_builtin(
                BuiltInProgramError::NotEnoughAccountKeys,
            ))
        }
    };

    let sys_prog_key = *accounts.get(sys_prog).key();
    Cpi::<MAX_CPI_ACCS>::new().invoke_fwd(
        accounts,
        &sys_prog_key,
        TransferIxData::new(trf_amt).as_buf(),
        transfer_accs.0,
    )?;

    Ok(())
}

Key Features

Account Handle System and Compile-Time Borrow Checking

Instead of using RefCells (maybe wrapped in an Rc) to implement dynamic borrow checking at runtime like the other libraries, jiminy solves the issue of aliasing duplicated accounts at compile-time. It does so by encapsulating all deserialized accounts into a common Accounts collection that only allows at any time, either a single Account to be mutably borrowed or multiple Accounts to be immutably borrowed, just as it is with any other safe collections data structure (Vec, HashMap etc) in Rust. Access is granted via dispensed AccountHandles, which can be held across mutable accesses since they are inert until used.

fn process_ix(
    accounts: &mut Accounts,
    _data: &[u8],
    _prog_id: &[u8; 32],
) -> Result<(), ProgramError> {
    let [handle_a, handle_b] = *accounts.as_slice() else {
        return Err(ProgramError::from_builtin(
            BuiltInProgramError::NotEnoughAccountKeys,
        ));
    };
    let a = accounts.get_mut(handle_a);
    let b = accounts.get(handle_b);
    a.dec_lamports(1); // this fails to compile with "cannot borrow accounts as immutable because it is also borrowed as mutable"
    Ok(())
}

The result of this is higher performance and smaller binary sizes because all the previous code related to handling of RefCells is no longer required. Say goodbye to the dreaded AccountBorrowFailed error.

NonZeroU64 as ProgramError

In jiminy, ProgramError is simply defined as:

#[repr(transparent)]
pub struct ProgramError(pub NonZeroU64);

This simple change of representation allows for much more optimized bytecode to be generated by the compiler, as it removes many unnecessary conversion steps between more complex enum variants and the final return value to put into r0 when the ebpf program exits.

At the same time, convenience functions are still provided for easily creating the built-in errors.

ProgramError::from_builtin(BuiltInProgramError::InvalidArgument)

CPI Overhaul

Reusable CPI Buffers

The CPI syscall expects inputs in a very specific format. Most of the work of each library's invoke_signed() function is in converting program input data into the correct format and accumulating them in buffers so that they can be passed to the syscall. jiminy allows these buffers to be reused, minimizing memory footprint.

// simply Box::new() this if more space is
// required than what is available on the stack.
let mut cpi: Cpi = Cpi::new();

cpi.invoke_fwd(
    accounts,
    &sys_prog_key,
    TransferIxData::new(ONE_SOL_IN_LAMPORTS).as_buf(),
    transfer_accs.0,
)?;

// use the same allocation again for a completely different CPI
cpi.invoke_signed(
    accounts,
    &sys_prog_key,
    &AssignIxData::new(prog_id)
    assign_accs.into_account_handle_perms(),
    &[],
)?;

Benchmarks

Current eisodos benchmark results

Development

This section contains dev info for people who wish to work on the library.

Solana Versions

Toolchain

$ cargo-build-sbf --version
solana-cargo-build-sbf 2.3.7
platform-tools v1.48
rustc 1.84.1

References

About

Yet another no-std no-dependencies library for writing solana programs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published