pinocchio-development

Par elophanto · elophanto

Guide complet pour créer des programmes Solana haute performance avec Pinocchio — le framework zéro dépendance et zéro copie. Couvre la validation de comptes, les patterns CPI, les techniques d'optimisation et la migration depuis Anchor.

npx skills add https://github.com/elophanto/elophanto --skill pinocchio-development

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

  1. Toujours utiliser des discriminateurs un octet pour les instructions et les comptes
  2. Préférer bytemuck à Borsh pour les données de taille fixe
  3. Utiliser lazy_entrypoint! pour les programmes à instruction unique
  4. Valider tous les comptes avant le traitement
  5. Utiliser invoke_signed pour les opérations sur comptes possédés par PDA
  6. Ajouter du padding pour aligner les structs à 8 octets
  7. Tester avec solana-program-test ou 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 (simulateTransaction ou é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 getSignatureStatuses ou 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" et pinocchio-token = "0.4" pour les aides CPI
  • Le support Token-2022 via pinocchio-token est 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)

Skills similaires