Rust
Introduction to Pinocchio

Introduction to Pinocchio

Instructions

As we saw earlier, using the TryFrom trait allows us to cleanly separate validation from business logic, improving both maintainability and security.

Instruction Structure

When it's time to process the logic, we can create a structure like this:

pub struct Deposit<'a> {
    pub accounts: DepositAccounts<'a>,
    pub instruction_datas: DepositInstructionData,
}

This structure defines what data will be accessible during the logic processing. We then deserialize this using the try_from function that you can find in the lib.rs file:

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
    type Error = ProgramError;
 
    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = DepositAccounts::try_from(accounts)?;
        let instruction_datas = DepositInstructionData::try_from(data)?;
 
        Ok(Self {
            accounts,
            instruction_datas,
        })
    }
}

This wrapper provides three key benefits:

  1. It accepts both raw inputs (bytes and accounts)
  2. It delegates validation to the individual TryFrom implementations
  3. It returns a fully-typed, fully-validated Deposit struct

We can then implement the processing logic like this:

impl<'a> Deposit<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;
 
    pub fn process(&self) -> ProgramResult {
        // deposit logic
        Ok(())
    }
}
  • The DISCRIMINATOR is the byte we use for pattern matching in the entrypoint
  • The process() method contains only business logic, as all validation checks are already complete

The result? We get Anchor-style ergonomics with all the benefits of being fully native: explicit, predictable, and fast.

Cross Program Invocation

As mentioned earlier, Pinocchio provides helper crates like pinocchio-system and pinocchio-token that simplify Cross-Program Invocations (CPIs) to native programs.

These helper structs and methods replace Anchor's CpiContext approach we used previously:

Transfer {
    from: self.accounts.owner,
    to: self.accounts.vault,
    lamports: self.instruction_datas.amount,
}
.invoke()?;

The Transfer struct (from pinocchio-system) encapsulates all the fields required by the System Program, and .invoke() executes the CPI. No context builder or extra boilerplate needed.

When the caller must be a Program-Derived Address (PDA), Pinocchio maintains the same concise API:

let seeds = [
    Seed::from(b"vault"),
    Seed::from(self.accounts.owner.key().as_ref()),
    Seed::from(&[bump]),
];
let signers = [Signer::from(&seeds)];
 
Transfer {
    from: self.accounts.vault,
    to: self.accounts.owner,
    lamports: self.accounts.vault.lamports(),
}
.invoke_signed(&signers)?;

Here's how it works:

  1. Seeds creates an array of Seed objects that match the PDA derivation
  2. Signer wraps these seeds in a Signer helper
  3. invoke_signed executes the CPI, passing the signer array to authorize the transfer

The result? A clean, first-class interface for both regular and signed CPIs: no macros required, and no hidden magic.

Multiple Instruction Structure

Often, you’ll want to reuse the same account structure and validation logic across several instructions; such as when updating different configuration fields.

Instead of creating a separate discriminator for each instruction, you can use a pattern that distinguishes instructions by the size of their data payload.

Here’s how it works:

We use a single instruction discriminator for all related config updates. The specific instruction is determined by the length of the incoming data.

After that, in your processor, match on self.data.len(). Each instruction type has a unique data size, so you can dispatch to the correct handler accordingly.

It will look like this:

pub struct UpdateConfig<'a> {
    pub accounts: UpdateConfigAccounts<'a>,
    pub data: &'a [u8],
}
 
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for UpdateConfig<'a> {
    type Error = ProgramError;
 
    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = UpdateConfigAccounts::try_from(accounts)?;
 
        // Return the initialized struct
        Ok(Self { accounts, data })
    }
}
 
impl<'a> UpdateConfig<'a> {
    pub const DISCRIMINATOR: &'a u8 = &4;
 
    pub fn process(&mut self) -> ProgramResult {
        match self.data.len() {
            len if len == size_of::<UpdateConfigStatusInstructionData>() => {
                self.process_update_status()
            }
            len if len == size_of::<UpdateConfigFeeInstructionData>() => self.process_update_fee(),
            len if len == size_of::<UpdateConfigAuthorityInstructionData>() => {
                self.process_update_authority()
            }
            _ => Err(ProgramError::InvalidInstructionData),
        }
    }
 
    //..
}

Notice that we defer deserialization of the instruction data until after we know which handler to call. This avoids unnecessary parsing and keeps the entrypoint logic clean.

Each handler then can deserialize its specific data type and performs the update:

pub fn process_update_authority(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigAuthorityInstructionData::try_from(self.data)?;
 
    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;
 
    unsafe { config.set_authority_unchecked(instruction_data.authority) }?;
 
    Ok(())
}
 
pub fn process_update_fee(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigFeeInstructionData::try_from(self.data)?;
 
    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;
 
    unsafe { config.set_fee_unchecked(instruction_data.fee) }?;
 
    Ok(())
}
 
pub fn process_update_status(&mut self) -> ProgramResult {
    let instruction_data = UpdateConfigStatusInstructionData::try_from(self.data)?;
 
    let mut data = self.accounts.config.try_borrow_mut_data()?;
    let config = Config::load_mut_unchecked(&mut data)?;
 
    unsafe { config.set_state_unchecked(instruction_data.status) }?;
 
    Ok(())
}

This approach lets you share account validation and use a single entrypoint for multiple related instructions, reducing boilerplate and making your codebase easier to maintain.

By pattern matching on data size, you can efficiently route to the correct logic without extra discriminators or complex parsing.

Contents
View Source
Blueshift © 2025Commit: e508535