Yet another no-std no-dependencies library for writing solana programs.
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(())
}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.
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);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(())
}Current eisodos benchmark results
This section contains dev info for people who wish to work on the library.
$ cargo-build-sbf --version
solana-cargo-build-sbf 3.1.1
platform-tools v1.52
rustc 1.89.0Install with
$ sh -c "$(curl -sSfL https://release.anza.xyz/v3.1.1/install)"