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:

#![allow(unexpected_cfgs)]

use jiminy_entrypoint::{
    account::{Abr, AccountHandle},
    program_error::{
        INVALID_INSTRUCTION_DATA,
        NOT_ENOUGH_ACCOUNT_KEYS,
        ProgramError,
    }
};
use jiminy_system_prog_interface::{TransferIxAccs, TransferIxData};

// Determines the maximum number of accounts that can be deserialized and
// saved to the AccountHandle slice. 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;

type Cpi = jiminy_cpi::Cpi<MAX_CPI_ACCS>;

const ONE_SOL_IN_LAMPORTS: u64 = 1_000_000_000;

jiminy_entrypoint::entrypoint!(process_ix, MAX_ACCS);

fn process_ix(
    abr: &mut Abr,
    accounts: &[AccountHandle<'_>],
    data: &[u8],
    _prog_id: &[u8; 32],
) -> Result<(), ProgramError> {
    let (sys_prog, transfer_accs) = match accounts.split_last_chunk() {
        Some((&[sys_prog], ta)) => (sys_prog, TransferIxAccs(*ta)),
        _ => {
            return Err(NOT_ENOUGH_ACCOUNT_KEYS.into())
        }
    };

    let data: &[u8; 8] = data.try_into().map_err(|_| INVALID_INSTRUCTION_DATA)?;
    let trf_amt = u64::from_le_bytes(*data);

    let sys_prog_key = *abr.get(sys_prog).key();
    Cpi::new().invoke_fwd(
        abr,
        &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 creating a handle system for accounts, represented by jiminy_account::AccountHandle. These handles are inert until used to borrow an account. To borrow from a handle, the user must borrow the global singleton jiminy_account::Abr at the same time. So at any one time, either multiple accounts are immutably borrowed, or a single account is mutably borrowed.

use jiminy_account::{Abr, AccountHandle};
use jiminy_program_error::{NOT_ENOUGH_ACCOUNT_KEYS, ProgramError};

fn process_ix(
    abr: &mut Abr,
    accounts: &[AccountHandle<'_>],
    _data: &[u8],
    _prog_id: &[u8; 32],
) -> Result<(), ProgramError> {
    let [handle_a, handle_b] = *accounts else {
        return Err(NOT_ENOUGH_ACCOUNT_KEYS.into());
    };
    let a = abr.get_mut(handle_a);
    let b = abr.get(handle_b);
    a.dec_lamports(1); // this fails to compile with "cannot borrow abr 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:

use core::num::NonZeroU64;

#[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.

use jiminy_program_error::{INVALID_ARGUMENT, BuiltInProgramError, ProgramError};

ProgramError::from_builtin(BuiltInProgramError::InvalidArgument);

// ProgramError impls `From<NonZeroU64>`
ProgramError::from(INVALID_ARGUMENT);

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.

use jiminy_cpi::Cpi;
use jiminy_entrypoint::{
    account::{Abr, AccountHandle},
    program_error::{
        INVALID_INSTRUCTION_DATA,
        NOT_ENOUGH_ACCOUNT_KEYS,
        ProgramError,
    }
};
use jiminy_pda::{PdaSeed, PdaSigner};
use jiminy_system_prog_interface::{AssignIxAccs, AssignIxData, TransferIxAccs, TransferIxData};

const MY_SEED: &[u8] = b"myseed";
const BUMP: u8 = 254;

fn process_ix(
    abr: &mut Abr,
    accounts: &[AccountHandle<'_>],
    _data: &[u8],
    prog_id: &[u8; 32],
) -> Result<(), ProgramError> {
    let (transfer_accs, assign_accs, sys_prog) = match accounts
        .split_first_chunk()
        .and_then(|(t, s)| s.split_first_chunk().map(|(a, s)| (t, a, s))) {
            Some((t, a, &[sys_prog, ..])) => (TransferIxAccs(*t), AssignIxAccs(*a), sys_prog),
            _ => {
                return Err(NOT_ENOUGH_ACCOUNT_KEYS.into())
            }
    };

    // 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_handle(
        abr,
        sys_prog,
        TransferIxData::new(1_000_000_000).as_buf(),
        transfer_accs.0,
    )?;

    // use the same allocation again for a completely different CPI
    cpi.invoke_signed_handle(
        abr,
        sys_prog,
        AssignIxData::new(prog_id).as_buf(),
        assign_accs.into_account_handle_perms(),
        &[
            PdaSigner::new(&[
                PdaSeed::new(MY_SEED),
                PdaSeed::new(core::slice::from_ref(&BUMP)),
            ])
        ],
    )?;

    Ok(())
}

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 3.1.1
platform-tools v1.52
rustc 1.89.0

Install with

$ sh -c "$(curl -sSfL https://release.anza.xyz/v3.1.1/install)"

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

Contributors 3

  •  
  •  
  •