Skip to content

Commit

Permalink
Add lazy_entrypoint macro (#29)
Browse files Browse the repository at this point in the history
* Add lazy_entrypoint

* Move constants to crate root

* Fix clippy

* Add get account helper

* Add entrypoint information

* Expand docs

* Cosmetics

* Typo

* Remove nested function call

* Remove unsupported methods

* Cleanup

* Add reexport
  • Loading branch information
febo authored Oct 25, 2024
1 parent f82801a commit 1e58c2e
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 41 deletions.
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The library defines:
* core data types
* logging macros
* `syscall` functions
* access to system accounts (`sysvar`)
* access to system accounts (`sysvars`)
* cross-program invocation

## Features
Expand All @@ -51,13 +51,22 @@ From your project folder:
cargo add pinocchio
```

On your entrypoint definition:
Pinocchio provides two different entrypoint macros: an `entrypoint` that looks similar to the "standard" one found in `solana-program` and a lightweight `lazy_entrypoint`. The main difference between them is how much work the entrypoint performs. While the `entrypoint` parsers the whole input and provide the `program_id`, `accounts` and `instruction_data` separately, the `lazy_entrypoint` only wraps the input at first. It then provides methods to parse the input on demand. The benefit in this case is that you have more control when the parsing is happening — even whether the parsing is needed or not.

The `lazy_entrypoint` is suitable for programs that have a single or very few instructions, since it requires the program to handle the parsing, which can become complex as the number of instructions increases. For "larger" programs, the `entrypoint` will likely be easier and more efficient to use.

> ⚠️ **Note:**
> In both cases you should use the types from the `pinocchio` crate instead of `solana-program`. If you need to invoke a different program, you will need to redefine its instruction builder to create an equivalent instruction data using `pinocchio` types.
### 🚪 `entrypoint!`

To use the `entrypoint!` macro, use the following in your entrypoint definition:
```rust
use pinocchio::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
ProgramResult
pubkey::Pubkey
};

Expand All @@ -73,8 +82,38 @@ pub fn process_instruction(
}
```

> ⚠️ **Note:**
> You should use the types from the `pinocchio` crate instead of `solana-program`. If you need to invoke a different program, you will need to redefine its instruction builder to create an equivalent instruction data using `pinocchio` types.
The information from the input is parsed into their own entities:

* `program_id`: the `ID` of the program being called
* `accounts`: the accounts received
* `instruction_data`: data for the instruction

### 🚪 `lazy_entrypoint!`

To use the `lazy_entrypoint!` macro, use the following in your entrypoint definition:
```rust
use pinocchio::{
lazy_entrypoint,
lazy_entrypoint::InstructionContext,
msg,
ProgramResult
};

lazy_entrypoint!(process_instruction);

pub fn process_instruction(
mut context: InstructionContext,
) -> ProgramResult {
msg!("Hello from my lazy program!");
Ok(())
}
```

The `InstructionContext` provides on-demand access to the information of the input:

* `available()`: number of available accounts
* `next_account()`: parsers the next available account (can be used as many times as accounts available)
* `instruction_data()`: parsers the intruction data and program id

## License

Expand Down
38 changes: 12 additions & 26 deletions sdk/pinocchio/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use core::{alloc::Layout, mem::size_of, ptr::null_mut, slice::from_raw_parts};

use crate::{
account_info::{Account, AccountInfo, MAX_PERMITTED_DATA_INCREASE},
program_error::ProgramError,
pubkey::Pubkey,
BPF_ALIGN_OF_U128, NON_DUP_MARKER,
};

/// Start address of the memory region used for program heap.
Expand All @@ -15,29 +15,16 @@ pub const HEAP_START_ADDRESS: u64 = 0x300000000;
/// Length of the heap memory region used for program heap.
pub const HEAP_LENGTH: usize = 32 * 1024;

/// Maximum number of accounts that a transaction may process.
///
/// This value is used to set the maximum number of accounts that a program
/// is expecting and statically initialize the array of `AccountInfo`.
///
/// This is based on the current [maximum number of accounts] that a transaction
/// may lock in a block.
///
/// [maximum number of accounts]: https://github.com/anza-xyz/agave/blob/2e6ca8c1f62db62c1db7f19c9962d4db43d0d550/runtime/src/bank.rs#L3209-L3221
pub const MAX_TX_ACCOUNTS: usize = 128;

/// `assert_eq(core::mem::align_of::<u128>(), 8)` is true for BPF but not
/// for some host machines.
pub const BPF_ALIGN_OF_U128: usize = 8;

/// Value used to indicate that a serialized account is not a duplicate.
pub const NON_DUP_MARKER: u8 = u8::MAX;
#[deprecated(
since = "0.6.0",
note = "Use `ProgramResult` from the crate root instead"
)]
/// The result of a program execution.
pub type ProgramResult = super::ProgramResult;

#[deprecated(since = "0.6.0", note = "Use `SUCCESS` from the crate root instead")]
/// Return value for a successful program execution.
pub const SUCCESS: u64 = 0;

/// The result of a program execution.
pub type ProgramResult = Result<(), ProgramError>;
pub const SUCCESS: u64 = super::SUCCESS;

/// Declare the program entrypoint and set up global handlers.
///
Expand Down Expand Up @@ -81,9 +68,9 @@ pub type ProgramResult = Result<(), ProgramError>;
/// use pinocchio::{
/// account_info::AccountInfo,
/// entrypoint,
/// entrypoint::ProgramResult,
/// msg,
/// pubkey::Pubkey
/// pubkey::Pubkey,
/// ProgramResult
/// };
///
/// entrypoint!(process_instruction);
Expand All @@ -94,7 +81,6 @@ pub type ProgramResult = Result<(), ProgramError>;
/// instruction_data: &[u8],
/// ) -> ProgramResult {
/// msg!("Hello from my program!");
///
/// Ok(())
/// }
///
Expand Down Expand Up @@ -126,7 +112,7 @@ macro_rules! entrypoint {
core::slice::from_raw_parts(accounts.as_ptr() as _, count),
&instruction_data,
) {
Ok(()) => $crate::entrypoint::SUCCESS,
Ok(()) => $crate::SUCCESS,
Err(error) => error.into(),
}
}
Expand Down
227 changes: 227 additions & 0 deletions sdk/pinocchio/src/lazy_entrypoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use crate::{
account_info::{Account, AccountInfo, MAX_PERMITTED_DATA_INCREASE},
program_error::ProgramError,
pubkey::Pubkey,
BPF_ALIGN_OF_U128, NON_DUP_MARKER,
};

/// Declare the program entrypoint.
///
/// This entrypoint is defined as *lazy* because it does not read the accounts upfront
/// nor set up global handlers. Instead, it provides an [`InstructionContext`] to the
/// access input information on demand. This is useful when the program needs more control
/// over the compute units it uses. The trade-off is that the program is responsible for
/// managing potential duplicated accounts and set up a `global allocator`
/// and `panic handler`.
///
/// This macro emits the boilerplate necessary to begin program execution, calling a
/// provided function to process the program instruction supplied by the runtime, and reporting
/// its result to the runtime.
///
/// The only argument is the name of a function with this type signature:
///
/// ```ignore
/// fn process_instruction(
/// mut context: InstructionContext, // wrapper around the input buffer
/// ) -> ProgramResult;
/// ```
///
/// # Examples
///
/// Defining an entrypoint and making it conditional on the `bpf-entrypoint` feature. Although
/// the `entrypoint` module is written inline in this example, it is common to put it into its
/// own file.
///
/// ```no_run
/// #[cfg(feature = "bpf-entrypoint")]
/// pub mod entrypoint {
///
/// use pinocchio::{
/// lazy_entrypoint,
/// lazy_entrypoint::InstructionContext,
/// msg,
/// ProgramResult
/// };
///
/// lazy_entrypoint!(process_instruction);
///
/// pub fn process_instruction(
/// mut context: InstructionContext,
/// ) -> ProgramResult {
/// msg!("Hello from my lazy program!");
/// Ok(())
/// }
///
/// }
/// ```
#[macro_export]
macro_rules! lazy_entrypoint {
( $process_instruction:ident ) => {
/// Program entrypoint.
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
match $process_instruction($crate::lazy_entrypoint::InstructionContext::new(input)) {
Ok(_) => $crate::SUCCESS,
Err(error) => error.into(),
}
}
};
}

/// Context to access data from the input buffer for the instruction.
///
/// This is a wrapper around the input buffer that provides methods to read the accounts
/// and instruction data. It is used by the lazy entrypoint to access the input data on demand.
pub struct InstructionContext {
/// Pointer to the runtime input buffer for the instruction.
input: *mut u8,

/// Number of remaining accounts.
///
/// This value is decremented each time [`next_account`] is called.
remaining: u64,

/// Current memory offset on the input buffer.
offset: usize,
}

impl InstructionContext {
/// Creates a new [`InstructionContext`] for the input buffer.
#[inline(always)]
pub fn new(input: *mut u8) -> Self {
Self {
input,
remaining: unsafe { *(input as *const u64) },
offset: core::mem::size_of::<u64>(),
}
}

/// Reads the next account for the instruction.
///
/// The account is represented as a [`MaybeAccount`], since it can either
/// represent and [`AccountInfo`] or the index of a duplicated account. It is up to the
/// caller to handle the mapping back to the source account.
///
/// # Error
///
/// Returns a [`ProgramError::NotEnoughAccountKeys`] error if there are
/// no remaining accounts.
#[inline(always)]
pub fn next_account(&mut self) -> Result<MaybeAccount, ProgramError> {
self.remaining = self
.remaining
.checked_sub(1)
.ok_or(ProgramError::NotEnoughAccountKeys)?;

Ok(unsafe { read_account(self.input, &mut self.offset) })
}

/// Returns the next account for the instruction.
///
/// Note that this method does *not* decrement the number of remaining accounts, but moves
/// the offset forward. It is intended for use when the caller is certain on the number of
/// remaining accounts.
///
/// # Safety
///
/// It is up to the caller to guarantee that there are remaining accounts; calling this when
/// there are no more remaining accounts results in undefined behavior.
#[inline(always)]
pub unsafe fn next_account_unchecked(&mut self) -> MaybeAccount {
read_account(self.input, &mut self.offset)
}

/// Returns the number of available accounts.
#[inline(always)]
pub fn available(&self) -> u64 {
unsafe { *(self.input as *const u64) }
}

/// Returns the number of remaining accounts.
///
/// This value is decremented each time [`next_account`] is called.
#[inline(always)]
pub fn remaining(&self) -> u64 {
self.remaining
}

/// Returns the instruction data for the instruction.
///
/// This method can only be used after all accounts have been read; otherwise, it will
/// return a [`ProgramError::InvalidInstructionData`] error.
#[inline(always)]
pub fn instruction_data(&mut self) -> Result<(&[u8], &Pubkey), ProgramError> {
if self.remaining > 0 {
return Err(ProgramError::InvalidInstructionData);
}

Ok(unsafe { self.instruction_data_unchecked() })
}

/// Returns the instruction data for the instruction.
///
/// # Safety
///
/// It is up to the caller to guarantee that all accounts have been read; calling this method
/// before reading all accounts will result in undefined behavior.
#[inline(always)]
pub unsafe fn instruction_data_unchecked(&mut self) -> (&[u8], &Pubkey) {
let data_len = *(self.input.add(self.offset) as *const usize);
// shadowing the offset to avoid leaving it in an inconsistent state
let offset = self.offset + core::mem::size_of::<u64>();
let data = core::slice::from_raw_parts(self.input.add(offset), data_len);

(data, &*(self.input.add(offset + data_len) as *const Pubkey))
}
}

/// Wrapper type around an [`AccountInfo`] that may be a duplicate.
pub enum MaybeAccount {
/// An [`AccountInfo`] that is not a duplicate.
Account(AccountInfo),

/// The index of the original account that was duplicated.
Duplicated(u8),
}

impl MaybeAccount {
/// Extracts the wrapped [`AccountInfo`].
///
/// It is up to the caller to guarantee that the [`MaybeAccount`] really is in an
/// [`MaybeAccount::Account`]. Calling this method when the variant is a
/// [`MaybeAccount::Duplicated`] will result in a panic.
#[inline(always)]
pub fn assume_account(self) -> AccountInfo {
let MaybeAccount::Account(account) = self else {
panic!("Duplicated account")
};
account
}
}

/// Read an account from the input buffer.
///
/// This can only be called with a buffer that was serialized by the runtime as
/// it assumes a specific memory layout.
#[allow(clippy::cast_ptr_alignment, clippy::missing_safety_doc)]
#[inline(always)]
unsafe fn read_account(input: *mut u8, offset: &mut usize) -> MaybeAccount {
let account: *mut Account = input.add(*offset) as *mut _;

if (*account).borrow_state == NON_DUP_MARKER {
// repurpose the borrow state to track borrows
(*account).borrow_state = 0b_0000_0000;

*offset += core::mem::size_of::<Account>();
*offset += (*account).data_len as usize;
*offset += MAX_PERMITTED_DATA_INCREASE;
*offset += (*offset as *const u8).align_offset(BPF_ALIGN_OF_U128);
*offset += core::mem::size_of::<u64>();

MaybeAccount::Account(AccountInfo { raw: account })
} else {
*offset += core::mem::size_of::<u64>();
//the caller will handle the mapping to the original account
MaybeAccount::Duplicated((*account).borrow_state)
}
}
Loading

0 comments on commit 1e58c2e

Please sign in to comment.