build solana smart contracts with pinocchio
/Development

How to Build Solana Programs with Pinocchio

12 min read

Pinocchio is a highly optimized, zero-dependency library that can be used to build native Solana programs. Pinocchio was created by Anza, the core developers of Solana’s Agave client. 

Exo Tech is a leading Solana developer shop that has been one of the earliest adopters of Pinocchio. Through our client work, we’ve developed multiple production programs using Pinocchio and have contributed to the SDK to add missing functionality. 

This article provides an in-depth look at building programs with Pinocchio, exploring the benefits and tradeoffs. We aim to equip developers with the knowledge to determine if Pinocchio is a good fit for their program. However, it is important to note that Pinocchio is not beginner friendly as it prioritizes optimization over developer experience.

What is the Pinocchio library?

The Pinocchio library is a replacement for the solana-program crate that optimizes program execution by making extensive use of zero-copy types. zero-copy means that data does not need to be copied to a separate memory address when reading or writing, which saves compute resources (or CUs in Solana).

The library has zero dependencies and is “no_std”. Rust’s std crate provides common ways of accessing operating system resources as well as a runtime, but given the Solana Virtual Machine (SVM) is itself a runtime, this overhead is not necessary.

How is Pinocchio more performant than solana-program?

Every Solana program needs an entrypoint that the runtime calls for program execution. The solana-program library exposes the entrypoint! macro, which deserializes the program's input, sets up a heap allocator, and creates a panic handler.

Code
macro_rules! entrypoint {
    ($process_instruction:ident) => {
        /// # Safety
        #[no_mangle]
        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
            let (program_id, accounts, instruction_data) =
                unsafe { $crate::entrypoint::deserialize(input) };
            match $process_instruction(&program_id, &accounts, &instruction_data) {
                Ok(()) => $crate::entrypoint::SUCCESS,
                Err(error) => error.into(),
            }
        }
        $crate::custom_heap_default!();
        $crate::custom_panic_default!();
    };
}

Pinocchio exports three entrypoint macros.

For those migrating from solana-program, the entrypoint! macro will function mostly the same by deserializing the program input and setting up the allocator and handler. 

However, the other two macros decouple the entrypoint from the setup of the heap allocator and panic handler, giving the developer more control to omit or optimize before their program logic is executed. 

program_entrypoint! deserializes the program input similar to solana-program, while the lazy_program_entrypoint! simply wraps the input buffer and defers handling to the program, allowing for more control over compute. 

Since these macros do not set up the heap allocator or panic handler, the Pinocchio library exposes default macros for the developer to use.

Separately, if a program knows it will never need heap memory, the no_allocator! saves compute units (CUs) by foregoing the setup of a memory allocator.

How do Pinocchio entrypoints deserialize Solana program inputs differently?

We briefly mentioned how the entrypoint for both solana-program and Pinocchio deserializes the program input. Still, it’s important to understand how the deserializations differ, as this is where major CU savings come from. 

At first glance, the deserialized inputs passed to the program’s instruction handler look the same:

Code
/// solana-program and pinocchio both look the same
process_instruction(
         program_id: &Pubkey,
         accounts: &[AccountInfo],
         instruction_data: &[u8],
     ) -> ProgramResult

The key difference is in the implementation of AccountInfo

While solana-program writes data to an AccountInfo struct that owns the data, Pinocchio’s AccountInfo struct is itself just a pointer to the underlying input data that represents the account. This reduces the amount of data needed to be copied, saving a lot of CUs.

How does Pinocchio enable developers to optimize CUs?

Since the instruction processor receives references to pointers, developers using the Pinocchio library will notice that their logic rarely ever has ownership over the data they’re working with. 

This can easily be seen when attempting to access values on the AccountInfo. Reading the account’s public key with the key() method will return a reference to the Pubkey. This makes it cheaper to read account information throughout program execution and cheaper to mutate the account’s data.

Pinocchio CU Optimization Example: P-token

A great example that continues to make use of zero-copy for optimizations is the p-token program.

This program is written to be a replacement for the canonical SPL Token Program, but it uses Pinocchio to drastically reduce the amount of compute units for each transaction.

What you’ll quickly notice is that all state is accessed via pointers.

Rather than deserializing the token account, the data from the AccountInfo is checked, and then a pointer is returned.

Each property is accessed via a function, and all values that are not primitives return a reference maintaining zero-copy. 

For more on why this drastically reduces the CU usage, check out this article on CU optimization.

Pinocchio vs. Anchor

Anchor is a very popular opinionated framework for developing Solana programs. It is considered higher level than Pinocchio as it does not contain logic to expose the underlying structures like AccountInfo.

Rather, Anchor depends on the previously mentioned solana-program crate and exposes traits and macros to streamline the program development process. Anchor provides instruction discriminator patterns and account deserialization logic. The deserialization logic relies on Borsh, which requires copying data to another memory address because it’s not zero-copy. 

While Anchor’s convenience speeds up the Solana program development process, it comes at the cost of more CU usage.

Pinocchio, on the other hand, is a library meant to replace solana-program for when developers require fine-tuned compute usage. It is completely unopinionated and allows the developer to structure the program in whatever manner they deem fit. Each Pinocchio project layout may look totally different, while Anchor projects have clearly defined structures. 

The Pinocchio library does not handle any client bindings or implementation. Anchor, on the other hand, has first class support for IDL generation, which can be used client side to interact with the program.

Developers using Pinocchio must write their own or use other tools like Shank and Codama, which we outline below in the Complimentary Tools for Building with Pinocchio section.

Pinocchio vs. Steel

Steel is another framework for writing Solana programs. Currently built on top of solana-program, Steel exposes macros, functions, and patterns that make it easy to write safe and expressive programs.

Steel’s opinionated nature makes it easy to read, while maintaining modularity. Developers may choose to use only the components of Steel that they need, unlike Anchor, which is an all-or-none framework.

Steel’s account! macro uses bytemuck for parsing the account structures, while Pinocchio does not handle account parsing at all. This also includes chainable parsers and assertions, making it simple to add custom validations. Pinocchio has no such patterns out of the box and would require the developer to write their own validation patterns.

However, when it comes to common cross-program invocations (CPIs) like the System Program and Token Program, both Pinocchio and Steel expose patterns that make such invocations easy.

Pinocchio is highly optimized, but leaves every detail up to the developer. Steel is a nice modular wrapper around the solana-program library designed to improve the developer experience.

How to Create a Token Using Pinocchio

To demonstrate a program written in Pinocchio, we’re going to rewrite the create token program from the Solana developer examples.

This is a simplistic program with a single instruction that creates a Token2022 token mint and uses the Metadata token extension to store information about the token. The metadata will be supplied via instruction data containing a name, symbol, and uri.

1. Define an Entrypoint

Let’s start by defining our program’s entrypoint.

We’re using the full entrypoint macro since we want to use Pinocchio’s default allocator and panic handling.

Code
entrypoint!(process_instruction);

fn process_instruction(
   _program_id: &Pubkey,
   accounts: &[AccountInfo],
   instruction_data: &[u8],
) -> ProgramResult {
   Ok(())
}

2. Define the Instruction Data Structure

Next we define the structure of our instruction data matching that of the other example programs. To save development time we’re going to use Borsh for deserialization and will leave more optimal deserialization methods for another article.

Code
#[derive(BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
   pub name: String,
   pub symbol: String,
   pub uri: String,
   pub decimals: u8,
}

3. Parse Accounts and Instruction Data

Now to write the logic within our instruction processor.

The first thing we must do is destructure the accounts from the account list and deserialize the instruction data into our CreateTokenArgs.

Code
let [mint_account, mint_authority, payer, token_program, _system_program] = accounts else {
       return Err(ProgramError::NotEnoughAccountKeys);
   };

   let args = CreateTokenArgs::try_from_slice(instruction_data)
       .map_err(|_| ProgramError::InvalidInstructionData)?;

4. Create Token2022 Mint Account

With the accounts and instruction data parsed, we invoke the System program CreateAccount instruction.

Below we’re using the CreateAccount struct from the `pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invoke.

Unlike creating a normal SPL Token mint, we must determine the additional space required by the token extensions we are using.

The Metadata Pointer extension size is static, while the Token Metadata extension must be calculated dynamically based on the arguments that were supplied.

Code
 /// [4 (extension discriminator) + 32 (update_authority) + 32 (metadata)]
   const METADATA_POINTER_SIZE: usize = 4 + 32 + 32;
   /// [4 (extension discriminator) + 32 (update_authority) + 32 (mint) + 4 (size of name ) + 4 (size of symbol) + 4 (size of uri) + 4 (size of additional_metadata)]
   const METADATA_EXTENSION_BASE_SIZE: usize = 4 + 32 + 32 + 4 + 4 + 4 + 4;
   /// Padding used so that Mint and Account extensions start at the same index
   const EXTENSIONS_PADDING_AND_OFFSET: usize = 84;


   /* within `process_instruction` */
   let extension_size = METADATA_POINTER_SIZE
       + METADATA_EXTENSION_BASE_SIZE
       + args.name.len()
       + args.symbol.len()
       + args.uri.len();
   let total_mint_size = Mint::LEN + EXTENSIONS_PADDING_AND_OFFSET + extension_size;

   let rent = Rent::get()?;
   // Create the account for the Mint
   CreateAccount {
       from: payer,
       to: mint_account,
       owner: token2022_program.key(),
       lamports: rent.minimum_balance(Mint::LEN),
       space: Mint::LEN as u64,
   }
   .invoke()?;

After CreateAccount has been invoked, the SystemProgram has registered the Token2022 program as the owner of the mint Account.

5. Initialize the Extension, Account, and Metadata Values

Next we must set the account data by initializing the Metadata Pointer extension, initializing the Mint account with the Token2022 program, and initializing the metadata values our program received as arguments. 

The following CPIs come from a branch of the pinocchio_token crate under active development. So it’s worth noting that this code is likely to be stale as the Token2022 functionality is planned to be separated from the SPL Token crate.

Code
// Initialize MetadataPointer extension pointing to the Mint account
   InitializeMetadataPointer {
       mint: mint_account,
       authority: Some(*payer.key()),
       metadata_address: Some(*mint_account.key()),
   }
   .invoke()?;

   // Now initialize that account as a Token2022 Mint
   InitializeMint2 {
       mint: mint_account,
       decimals: args.decimals,
       mint_authority: mint_authority.key(),
       freeze_authority: None,
   }
   .invoke(TokenProgramVariant::Token2022)?;

   // Set the metadata within the Mint account
   InitializeTokenMetadata {
       metadata: mint_account,
       update_authority: payer,
       mint: mint_account,
       mint_authority: payer,
       name: &args.name,
       symbol: &args.symbol,
       uri: &args.uri,
   }
   .invoke()?;

That’s it! 

Now we have a token mint with self-contained metadata using Token2022 written with Pinocchio.

There is room for improvement on this code to ensure max optimizations, but we hope that it gives you an understanding of how to write programs with Pinocchio.

Complimentary Tools for Building with Pinocchio

Specific tooling for Pinocchio is minimal, but growing.

Bytemuck for (De)serializing Accounts

Account (de)serialization must be implemented by the developer of a Pinocchio program. This is a tedious and error-prone process when done by hand. Bytemuck is a great library that makes it easy to read and write byte arrays as structs. This means that it’s fairly optimized by limiting the amount of data that needs to be copied into memory.

Borsh is another solution when working with accounts that do not have fixed sizes, though it’s less compute friendly and one of the reasons why people choose to use Pinocchio over Anchor.

Shank for Generating IDLs

Since Pinocchio is a library, it does not have built-in IDL generation like Anchor. An IDL (Interface Definition Language) is a JSON file that defines the public interface of a Solana program, including its instructions, account structures, and error codes, enabling standardized interactions and simplified client-side development.

For IDL generation, we recommend using Shank. This crate makes it incredibly easy for developers to annotate their code and use a CLI to generate a valid IDL. Adding the ShankAccount macro to the derive statement of a struct indicates that it’s an Account that should be (de)serializable. Once the shank CLI is run, this structure will end up as a typed account in the IDL to then be used for client generation.

Another important macro is the ShankInstruction for the program’s instruction enum, allowing an #[account] attribute to be used for indicating the index and permissions of each account in the list for that specific instruction.

See the shank-macro repository for more information on the useful code annotations that make IDL generation for non-Anchor programs easy.

Codama for Generating Clients

Once you have an IDL, client generation becomes easy with Codama. If the generated code doesn’t fit your needs, then you will need to write the clients manually.

At Exo Tech, we put together a Pinocchio project template to help us spin up Solana program repositories quickly. Feel free to give it a try and open a pull request for any improvements!

The Future of Pinocchio

While intended to be a drop in replacement for solana-program, Pinocchio is not yet at feature parity. Some sysvars are not yet supported, and non-core crates do not yet have full support or are non-existent. For example, multiple signers are not supported in the Pinocchio Token program crate. There is also no support for Token2022, though it is under development.

One of the more significant drawbacks of using Pinocchio is that all SDKs developed for other Solana programs use the solana-program crate. This means that each SDK requires ownership over the AccountInfo or data being passed around, which makes it incredibly difficult to interoperate with a Pinocchio-developed program. 

When integrating with third-party programs, it’s very common to have to write custom CPI logic for each instruction. This may eventually be resolved with code generators like Codama, but it’s not quite there.

It’s important to note that Pinocchio is still under active development and unaudited. The community is still working on including the rest of the sysvars to the SDK as well as improving support for important SPL programs like Token and Token2022.

How to Contribute to Pinocchio

There’s plenty of low hanging fruit for contributions to Pinocchio.

There are open issues and existing pull requests that could use additional support. Jump into the conversations or simply open a pull request for maintainers to review!

Conclusion

Pinocchio is a drastically more performant library for writing Solana programs compared to previous solutions. Giving developers more flexibility over their program’s entrypoint and using zero-copy for accessing program inputs can help developers reduce the CU usage. However, it is still a new library and not yet feature complete. The library is unaudited as of this writing, so use it with caution.

When evaluating whether to use Pinocchio or not, it is important to weigh the tradeoffs between other libraries and frameworks.

Opinionated frameworks like Anchor will speed up program development and be easier to maintain, making them an excellent choice when speed to market is important.

Once your product is stable and receiving a large volume of transactions, then optimizing Solana programs with a library like Pinocchio may be more suitable.

Additional Resources

For more information, watch Febo's presentation at Solana Accelerate 2025 and explore these educational resources:

Related Articles

Optimizing Solana Programs

A guide on Solana program optimization, covering Anchor to low-level unsafe Rust, balancing performance, safety, and usability.

Subscribe to Helius

Stay up-to-date with the latest in Solana development and receive updates when we post