Rust
Pinocchio para Iniciantes

Pinocchio para Iniciantes

Pinocchio 101

Introdução ao Pinocchio

O que é Pinocchio

Enquanto a maioria dos desenvolvedores Solana confia no Anchor, há muitas boas razões para escrever um programa sem ele. Talvez você precise de controle mais fino sobre cada campo de conta, ou esteja buscando máximo desempenho, ou simplesmente queira evitar macros.

Escrever programas Solana sem um framework como Anchor é conhecido como desenvolvimento nativo. É mais exigente, mas neste curso você aprenderá a criar um programa Solana do zero com Pinocchio; uma biblioteca leve que permite pular frameworks externos e ter controle de cada byte do seu código.

Pinocchio é uma biblioteca Rust minimalista que permite criar programas Solana sem puxar o pesado crate solana-program. Funciona tratando o payload da transação recebida (contas, dados de instrução, tudo) como um único slice de bytes e lendo-o in-place através de técnicas de zero-copy.

Vantagens principais

O design minimalista desbloqueia três grandes benefícios:

  • Menos unidades de compute. Sem desserialização extra ou cópias de memória.

  • Binários menores. Caminhos de código mais enxutos significam um .so mais leve on-chain.

  • Zero dependência externa. Sem crates externos para atualizar (ou quebrar).

O projeto foi iniciado por Febo na Anza com contribuição central do ecossistema Solana e da equipe Blueshift, e está disponível aqui.

Junto ao crate principal, você encontrará pinocchio-system e pinocchio-token, que fornecem helpers de zero-copy e utilitários CPI para os programas nativos System e SPL-Token da Solana.

Desenvolvimento Nativo

Desenvolvimento nativo pode parecer intimidador, mas é exatamente por isso que este capítulo existe. Ao final você entenderá cada byte que cruza o limite do programa e como manter sua lógica enxuta, segura e rápida.

Anchor usa Macros Procedurais e de Derivação para simplificar o boilerplate de lidar com contas, dados de instrução e tratamento de erros que são o núcleo da construção de Programas Solana.

Ir Nativo significa que não temos mais esse luxo e precisaremos:

  • Criar nosso próprio Discriminador e Entrypoint para as diferentes Instruções

  • Criar nossa própria lógica de Account, Instruction e desserialização

  • Implementar todas as verificações de segurança que o Anchor fazia por nós antes

Nota: Ainda não há um "framework" para construir programas Pinocchio. Por esta razão, vamos apresentar o que acreditamos ser a melhor forma de escrever programas pinocchio baseada em nossa experiência.

Entrypoint

No Anchor, a macro #[program] esconde muita ligação. Nos bastidores, ela constrói um discriminador de 8 bytes (tamanho customizável desde a versão 0.31) para cada instrução e struct de contas.

Anchor Discriminator Calculator
Account
sha256("account:" + PascalCase(seed))[0..8]
[0, 0, 0, 0, 0, 0, 0, 0]
Instruction
sha256("global:" + snake_case(seed))[0..8]
[0, 0, 0, 0, 0, 0, 0, 0]

Programas nativos geralmente mantêm as coisas mais enxutas. Um discriminador de byte único (valores 0x01…0xFF) é suficiente para até 255 instruções, o que é adequado para a maioria dos casos de uso. Se precisar de mais, pode mudar para uma variante de dois bytes, expandindo para 65.535 variantes possíveis.

A macro entrypoint! é onde a execução do programa começa. Ela fornece três slices brutos:

  • program_id: a chave pública do programa implantado

  • accounts: toda conta passada na instrução

  • instruction_data: um array de bytes opaco contendo seu discriminador mais quaisquer dados fornecidos pelo usuário

Isso significa que após o entrypoint podemos criar um padrão que executa todas as diferentes instruções através de um handler apropriado, que chamaremos de process_instruction. Veja como tipicamente fica:

rust
entrypoint!(process_instruction);

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {    
    match instruction_data.split_first() {
        Some((Instruction1::DISCRIMINATOR, data)) => Instruction1::try_from((data, accounts))?.process(),
        Some((Instruction2::DISCRIMINATOR, _)) => Instruction2::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData)
    }
}

Nos bastidos, este handler:

  1. Usa split_first() para extrair o byte discriminador

  2. Usa match para determinar qual struct de instrução instanciar

  3. A implementação try_from de cada instrução valida e desserializa suas entradas

  4. Uma chamada para process() executa a lógica de negócios

A diferença entre solana-program e pinocchio

A principal diferença e otimização está em como o entrypoint() se comporta.

  • Os entrypoints padrão da Solana usam padrões tradicionais de serialização onde o runtime desserializa os dados de entrada antecipadamente, criando estruturas de dados owned na memória. Esta abordagem usa serialização Borsh extensivamente, copia dados durante a desserialização e aloca memória para tipos de dados estruturados.

  • Entrypoints do Pinocchio implementam operações zero-copy lendo dados diretamente do array de bytes de entrada sem copiar. O framework define tipos zero-copy que referenciam os dados originais, elimina overhead de serialização/desserialização e usa acesso direto à memória para evitar camadas de abstração.

Contas e Instruções

Como não temos macros, e queremos evitá-las para manter o programa enxuto e eficiente, cada byte de dados de instrução e contas deve ser validado manualmente.

Para manter este processo organizado, usamos um padrão que fornece ergonomia no estilo Anchor sem macros, mantendo o método process() real praticamente livre de boilerplate implementando a trait TryFrom do Rust.

A Trait TryFrom

TryFrom é parte da família de conversões padrão do Rust. Diferente de From, que assume que uma conversão não pode falhar, TryFrom retorna um Result, permitindo que você surja erros cedo - perfeito para validação on-chain.

A trait é definida assim:

rust
pub trait TryFrom<T>: Sized {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

Em um programa Solana, implementamos TryFrom para converter slices brutos de contas (e, quando necessário, bytes de instrução) em structs fortemente tipadas enquanto impomos cada restrição.

Validação de Contas

Tipicamente lidamos com todas as verificações específicas que não requerem um double borrow (emprestando tanto na validação da conta quanto potencialmente no processamento) em cada implementação TryFrom. Isso mantém a função process(), onde toda a lógica da instrução acontece, a mais limpa possível.

Começamos implementando a struct de contas necessária para a instrução, similar ao Context do Anchor.

Nota: Diferente do Anchor, nesta struct de contas incluímos apenas as contas que queremos usar no processamento, e marcamos como _ as contas restantes que são necessárias na instrução mas não serão usadas (como o SystemProgram).

Para algo como um Vault, ficaria assim:

rust
pub struct DepositAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
}

Agora que sabemos quais contas queremos usar em nossa instrução, podemos usar a trait TryFrom para desserializar e realizar todas as verificações necessárias:

rust
impl<'a> TryFrom<&'a [AccountInfo]> for DepositAccounts<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        // 1. Destruturar o slice
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        // 2. Verificações customizadas
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        // 3. Retornar a struct validada
        Ok(Self { owner, vault })
    }
}

Como você pode ver, nesta instrução vamos usar uma CPI do SystemProgram para transferir lamports do owner para o vault, mas não precisamos usar o SystemProgram na instrução em si. O programa só precisa ser incluído na instrução, então podemos passá-lo como _.

Realizamos então verificações customizadas nas contas, similar às verificações Signer e SystemAccount do Anchor, e retornamos a struct validada.

Validação de Instrução

A validação de instrução segue um padrão similar à validação de contas. Usamos a trait TryFrom para validar e desserializar dados de instrução em structs fortemente tipadas, mantendo a lógica de negócios em process() limpa e focada.

Vamos começar definindo a struct que representa nossos dados de instrução:

rust
pub struct DepositInstructionData {
    pub amount: u64,
}

Implementamos então TryFrom para validar os dados da instrução e convertê-los em nosso tipo estruturado. Isso envolve:

  1. Verificar se o comprimento dos dados corresponde ao tamanho esperado

  2. Converter o slice de bytes em nosso tipo concreto

  3. Realizar quaisquer verificações de validação necessárias

Veja como fica a implementação:

rust
impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
    type Error = ProgramError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        // 1. Verificar se o comprimento dos dados corresponde a um u64 (8 bytes)
        if data.len() != core::mem::size_of::<u64>() {
            return Err(ProgramError::InvalidInstructionData);
        }

        // 2. Converter o slice de bytes para um u64
        let amount = u64::from_le_bytes(data.try_into().unwrap());

        // 3. Validar o valor (ex., garantir que não é zero)
        if amount == 0 {
            return Err(ProgramError::InvalidInstructionData);
        }

        Ok(Self { amount })
    }
}

Este padrão nos permite:

  • Validar dados de instrução antes que cheguem à lógica de negócios

  • Manter a lógica de validação separada da funcionalidade principal

  • Fornecer mensagens de erro claras quando a validação falha

  • Manter segurança de tipos ao longo do programa

Blueshift © 2026Commit: 1b88646