Guide de Développement Pinocchio
Construisez des programmes Solana ultra-rapides avec Pinocchio - un framework sans dépendance, sans copie qui livre une réduction des unités de calcul de 88-95 % et des binaires 40 % plus petits comparé aux approches traditionnelles.
Aperçu
Pinocchio est la bibliothèque Rust minimaliste d'Anza pour écrire des programmes Solana sans le lourd crate solana-program. Elle traite les données de transaction entrantes comme une seule tranche d'octets, les lisant sur place via des techniques zéro-copie.
Comparaison des Performances
| Métrique | Anchor | Native (solana-program) | Pinocchio |
|---|---|---|---|
| Token Transfer CU | ~6 000 | ~4 500 | ~600-800 |
| Taille du Binaire | Large | Moyen | Petit (-40%) |
| Allocation de Heap | Requise | Requise | Optionnelle |
| Dépendances | Nombreuses | Plusieurs | Zéro* |
*Uniquement les types Solana SDK pour l'exécution on-chain
Quand Utiliser Pinocchio
Utilisez Pinocchio Quand :
- Vous construisez des programmes à haut débit (DEX, carnets d'ordres, jeux)
- Les unités de calcul sont un goulot d'étranglement
- La taille du binaire compte (coûts de déploiement du programme)
- Vous avez besoin d'un contrôle maximal sur la mémoire
- Vous construisez une infrastructure (tokens, coffres-forts, séquestre)
Préférez Anchor Quand :
- Prototypage rapide / MVP
- Équipe non familière avec le Rust bas niveau
- Relations de compte complexes
- Besoin d'outils écosystème étendus
- Délai d'audit serré (plus d'auditeurs connaissent Anchor)
Démarrage Rapide
1. Configuration du Projet
# Cargo.toml
[package]
name = "my-program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[features]
default = []
bpf-entrypoint = []
[dependencies]
pinocchio = "0.10"
pinocchio-system = "0.4" # System Program CPI helpers
pinocchio-token = "0.4" # Token Program CPI helpers
bytemuck = { version = "1.14", features = ["derive"] }
[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
opt-level = 3
2. Structure de Programme de Base
use pinocchio::{
account_info::AccountInfo,
entrypoint,
program_error::ProgramError,
pubkey::Pubkey,
ProgramResult,
};
// Déclarer le point d'entrée
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Acheminer les instructions par discriminateur (premier octet)
match instruction_data.first() {
Some(0) => initialize(accounts, &instruction_data[1..]),
Some(1) => execute(accounts, &instruction_data[1..]),
_ => Err(ProgramError::InvalidInstructionData),
}
}
3. Définition de Compte avec Bytemuck
use bytemuck::{Pod, Zeroable};
// Discriminateur un octet pour le type de compte
pub const VAULT_DISCRIMINATOR: u8 = 1;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vault {
pub discriminator: u8,
pub owner: [u8; 32], // Pubkey en octets
pub balance: u64,
pub bump: u8,
pub _padding: [u8; 6], // Aligner à 8 octets
}
impl Vault {
pub const LEN: usize = std::mem::size_of::<Self>();
pub fn from_account(account: &AccountInfo) -> Result<&Self, ProgramError> {
let data = account.try_borrow_data()?;
if data.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if data[0] != VAULT_DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
Ok(bytemuck::from_bytes(&data[..Self::LEN]))
}
pub fn from_account_mut(account: &AccountInfo) -> Result<&mut Self, ProgramError> {
let mut data = account.try_borrow_mut_data()?;
if data.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(bytemuck::from_bytes_mut(&mut data[..Self::LEN]))
}
}
Instructions
Étape 1 : Définir la Validation de Compte
Créez une struct pour contenir les comptes validés :
pub struct InitializeAccounts<'a> {
pub vault: &'a AccountInfo,
pub owner: &'a AccountInfo,
pub system_program: &'a AccountInfo,
}
impl<'a> InitializeAccounts<'a> {
pub fn parse(accounts: &'a [AccountInfo]) -> Result<Self, ProgramError> {
let [vault, owner, system_program, ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Valider que le propriétaire est signataire
if !owner.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
// Valider le programme système
if system_program.key() != &pinocchio_system::ID {
return Err(ProgramError::IncorrectProgramId);
}
Ok(Self {
vault,
owner,
system_program,
})
}
}
Étape 2 : Implémenter le Gestionnaire d'Instruction
use pinocchio_system::instructions::CreateAccount;
pub fn initialize(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let ctx = InitializeAccounts::parse(accounts)?;
// Dériver la PDA
let (pda, bump) = Pubkey::find_program_address(
&[b"vault", ctx.owner.key().as_ref()],
&crate::ID,
);
// Vérifier que la PDA correspond
if ctx.vault.key() != &pda {
return Err(ProgramError::InvalidSeeds);
}
// Créer un compte via CPI
let space = Vault::LEN as u64;
let rent = pinocchio::sysvar::rent::Rent::get()?;
let lamports = rent.minimum_balance(space as usize);
CreateAccount {
from: ctx.owner,
to: ctx.vault,
lamports,
space,
owner: &crate::ID,
}
.invoke_signed(&[&[b"vault", ctx.owner.key().as_ref(), &[bump]]])?;
// Initialiser les données du compte
let vault = Vault::from_account_mut(ctx.vault)?;
vault.discriminator = VAULT_DISCRIMINATOR;
vault.owner = ctx.owner.key().to_bytes();
vault.balance = 0;
vault.bump = bump;
Ok(())
}
Options de Point d'Entrée
Pinocchio fournit trois macros de point d'entrée avec différents compromis :
1. Point d'Entrée Standard (Recommandé pour la plupart des cas)
use pinocchio::entrypoint;
entrypoint!(process_instruction);
- Configure l'allocateur de heap
- Configure le gestionnaire de panique
- Désérialise les comptes automatiquement
2. Point d'Entrée Lazy (Meilleur pour les programmes à instruction unique)
use pinocchio::lazy_entrypoint;
lazy_entrypoint!(process_instruction);
pub fn process_instruction(mut context: InstructionContext) -> ProgramResult {
// Comptes analysés à la demande
let account = context.next_account()?;
let data = context.instruction_data();
Ok(())
}
- Reporte l'analyse jusqu'à ce qu'elle soit nécessaire
- Meilleure économie de CU pour les programmes simples
- Réduction de 80-87 % des CU dans les repères du programme memo
3. Pas d'Allocateur (Optimisation Maximale)
use pinocchio::{entrypoint, no_allocator};
no_allocator!();
entrypoint!(process_instruction);
- Désactive le heap entièrement
- Impossible d'utiliser
String,Vec,Box - Meilleur pour les opérations de taille statique
Patterns CPI
CPI du Programme Système
use pinocchio_system::instructions::{CreateAccount, Transfer};
// Créer un compte
CreateAccount {
from: payer,
to: new_account,
lamports: rent_lamports,
space: account_size,
owner: &program_id,
}.invoke()?;
// Transférer SOL
Transfer {
from: source,
to: destination,
lamports: amount,
}.invoke()?;
// Transférer avec signataire PDA
Transfer {
from: pda_account,
to: destination,
lamports: amount,
}.invoke_signed(&[&[b"vault", owner.as_ref(), &[bump]]])?;
CPI du Programme Token
use pinocchio_token::instructions::{Transfer, MintTo, Burn};
// Transférer des tokens
Transfer {
source: from_token_account,
destination: to_token_account,
authority: owner,
amount: token_amount,
}.invoke()?;
// Frapper des tokens (avec autorité PDA)
MintTo {
mint: mint_account,
token_account: destination,
authority: mint_authority_pda,
amount: mint_amount,
}.invoke_signed(&[&[b"mint_auth", &[bump]]])?;
CPI Personnalisé (Programmes tiers)
use pinocchio::{
instruction::{AccountMeta, Instruction},
program::invoke,
};
// Construire l'instruction manuellement
let accounts = vec![
AccountMeta::new(*account1.key(), false),
AccountMeta::new_readonly(*account2.key(), true),
];
let ix = Instruction {
program_id: &external_program_id,
accounts: &accounts,
data: &instruction_data,
};
invoke(&ix, &[account1, account2])?;
Patterns de Validation de Compte
Pattern 1 : Trait TryFrom
pub struct DepositAccounts<'a> {
pub vault: &'a AccountInfo,
pub owner: &'a AccountInfo,
pub system_program: &'a AccountInfo,
}
impl<'a> TryFrom<&'a [AccountInfo]> for DepositAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
let [vault, owner, system_program, ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Validations
require!(owner.is_signer(), ProgramError::MissingRequiredSignature);
require!(vault.is_writable(), ProgramError::InvalidAccountData);
Ok(Self { vault, owner, system_program })
}
}
// Utilisation
let ctx = DepositAccounts::try_from(accounts)?;
Pattern 2 : Pattern Builder
pub struct AccountValidator<'a> {
account: &'a AccountInfo,
}
impl<'a> AccountValidator<'a> {
pub fn new(account: &'a AccountInfo) -> Self {
Self { account }
}
pub fn is_signer(self) -> Result<Self, ProgramError> {
if !self.account.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
Ok(self)
}
pub fn is_writable(self) -> Result<Self, ProgramError> {
if !self.account.is_writable() {
return Err(ProgramError::InvalidAccountData);
}
Ok(self)
}
pub fn has_owner(self, owner: &Pubkey) -> Result<Self, ProgramError> {
if self.account.owner() != owner {
return Err(ProgramError::IllegalOwner);
}
Ok(self)
}
pub fn build(self) -> &'a AccountInfo {
self.account
}
}
// Utilisation
let owner = AccountValidator::new(&accounts[0])
.is_signer()?
.is_writable()?
.build();
Pattern 3 : Validation Basée sur Macro
macro_rules! require {
($cond:expr, $err:expr) => {
if !$cond {
return Err($err);
}
};
}
macro_rules! require_signer {
($account:expr) => {
require!($account.is_signer(), ProgramError::MissingRequiredSignature)
};
}
macro_rules! require_writable {
($account:expr) => {
require!($account.is_writable(), ProgramError::InvalidAccountData)
};
}
Opérations PDA
Dériver des PDA
use pinocchio::pubkey::Pubkey;
// Trouver PDA avec bump
let (pda, bump) = Pubkey::find_program_address(
&[b"vault", user.key().as_ref()],
program_id,
);
// Créer PDA avec bump connu (moins cher)
let pda = Pubkey::create_program_address(
&[b"vault", user.key().as_ref(), &[bump]],
program_id,
)?;
Signature PDA pour CPI
// Ensemble de seed unique
let signer_seeds = &[b"vault", owner.as_ref(), &[bump]];
Transfer {
from: vault_pda,
to: destination,
lamports: amount,
}.invoke_signed(&[signer_seeds])?;
// Signataires PDA multiples
let signer1 = &[b"vault", owner.as_ref(), &[bump1]];
let signer2 = &[b"authority", &[bump2]];
invoke_signed(&ix, &accounts, &[signer1, signer2])?;
Sérialisation des Données
Taille Fixe avec Bytemuck (Recommandé)
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct GameState {
pub discriminator: u8,
pub player: [u8; 32],
pub score: u64,
pub level: u8,
pub _padding: [u8; 6],
}
// Lecture zéro-copie
let state: &GameState = bytemuck::from_bytes(&data);
// Écriture zéro-copie
let state: &mut GameState = bytemuck::from_bytes_mut(&mut data);
Taille Variable avec Borsh
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Metadata {
pub name: String,
pub symbol: String,
pub uri: String,
}
// Désérialiser (alloue)
let metadata = Metadata::try_from_slice(data)?;
// Sérialiser
let mut buffer = Vec::new();
metadata.serialize(&mut buffer)?;
Analyse Manuelle (Contrôle Maximal)
pub fn parse_u64(data: &[u8]) -> Result<u64, ProgramError> {
if data.len() < 8 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(u64::from_le_bytes(data[..8].try_into().unwrap()))
}
pub fn parse_pubkey(data: &[u8]) -> Result<Pubkey, ProgramError> {
if data.len() < 32 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Pubkey::new_from_array(data[..32].try_into().unwrap()))
}
Génération IDL avec Shank
Comme Pinocchio ne génère pas automatiquement les IDL, utilisez Shank :
use shank::{ShankAccount, ShankInstruction};
#[derive(ShankAccount)]
pub struct Vault {
pub owner: Pubkey,
pub balance: u64,
}
#[derive(ShankInstruction)]
pub enum ProgramInstruction {
#[account(0, writable, signer, name = "vault")]
#[account(1, signer, name = "owner")]
#[account(2, name = "system_program")]
Initialize,
#[account(0, writable, name = "vault")]
#[account(1, signer, name = "owner")]
Deposit { amount: u64 },
}
Générer l'IDL :
shank idl -o idl.json -p src/lib.rs
Directives
- Toujours utiliser des discriminateurs un octet pour les instructions et les comptes
- Préférer bytemuck à Borsh pour les données de taille fixe
- Utiliser
lazy_entrypoint!pour les programmes à instruction unique - Valider tous les comptes avant le traitement
- Utiliser
invoke_signedpour les opérations sur comptes possédés par PDA - Ajouter du padding pour aligner les structs à 8 octets
- Tester avec
solana-program-testou Bankrun
Fichiers dans Cette Compétence
pinocchio-development/
├── SKILL.md # Ce fichier
├── scripts/
│ ├── scaffold-program.sh # Générateur de projet
│ └── benchmark-cu.sh # Analyse comparative CU
├── resources/
│ ├── account-patterns.md # Patterns de validation
│ ├── cpi-reference.md # Référence rapide CPI
│ ├── optimization-checklist.md # Conseils de performance
│ └── anchor-comparison.md # Comparaison côte à côte
├── examples/
│ ├── counter/ # Programme compteur basique
│ ├── vault/ # Coffre-fort PDA avec dépôts
│ ├── token-operations/ # Frappe/transferts de tokens
│ └── transfer-hook/ # Crochet Token-2022
├── templates/
│ └── program-template.rs # Modèle de démarrage
└── docs/
├── migration-from-anchor.md # Guide de migration Anchor
└── edge-cases.md # Pièges et solutions
Repères de Performance (2025)
Les derniers repères démontrent l'efficacité de Pinocchio :
| Programme | Anchor CU | Pinocchio CU | Réduction |
|---|---|---|---|
| Token Transfer | ~6 000 | ~600-800 | 88-95% |
| Memo Program | ~650 | ~108 | 83% |
| Counter | ~800 | ~104 | 87% |
Implémentation Assembly : 104 CU, Pinocchio : 108 CU, Anchor Basique : 649 CU
Feuille de Route SDK (Plans Anza)
L'équipe Anza a annoncé des plans pour SDK v3 :
Améliorations à Venir
- Types de Base Unifiés : Types réutilisables entre Anchor et Pinocchio
- Nouvelle Bibliothèque de Sérialisation : Zéro-copie, énums plus simples, types de longueur variable
- Optimisation du Programme ATA : Compte Token Associé optimisé pour Pinocchio
- Optimisation Token22 : Support complet des Extensions Token avec utilisation CU minimale
Avancement de l'Intégration
- Les types Pinocchio sont intégrés dans le SDK Solana principal
- Interopérabilité améliorée entre les programmes Anchor et Pinocchio
Vérifier
- Un véritable appel RPC/SDK a été émis (mainnet, devnet ou validateur local) et la charge utile de réponse est capturée dans la transcription, non seulement paraphrasée
- Chaque transaction a été simulée (
simulateTransactionou équivalent) avant toute étape de signature/envoi ; les journaux de simulation sont joints - Pour toute transaction signée/envoyée, la signature résultante est enregistrée et confirmée en chaîne (statut renvoyé par
getSignatureStatusesou une URL d'explorateur) - Le slippage, les frais de priorité et les limites d'unités de calcul ont été définis explicitement avec des valeurs numériques concrètes, non laissés aux valeurs par défaut de la bibliothèque
- Les adresses de compte, les jetons et les ID de programme utilisés dans la exécution correspondent aux adresses pinocchio-programs documentées pour le cluster ciblé (pas de mélange mainnet/devnet)
- Le chemin d'échec a été exercé au moins une fois (solde insuffisant, oracle obsolète, blockhash expiré, etc.) et la gestion des erreurs de l'agent a produit un message lisible par un humain
Notes
- Pinocchio est non auditée - utilisez avec prudence en production
- La version 0.10.x est actuelle (dernière :
pinocchio = "0.10") pinocchio-system = "0.4"etpinocchio-token = "0.4"pour les aides CPI- Le support Token-2022 via
pinocchio-tokenest en développement actif - Pour la génération client, utilisez Codama avec votre IDL généré par Shank
- Maintenu par Anza (développeurs du client Solana Agave)