pinocchio-development について
pinocchio-development は、Pinocchio を使って Solana のオンチェーンプログラムを Rust で開発する人向けのガイドです。ゼロコピーのデータアクセスに加え、アカウント検証、CPI、PDA、シリアライズの実装パターンを扱います。
主な機能
- 01ゼロコピーによるアカウントデータ参照
- 02標準・遅延・アロケーターなしのエントリポイント
- 03アカウント検証と CPI のパターン
- 04PDA の導出と署名
- 05固定長・可変長データの解析
活用シーン
- 01計算ユニットの制約が厳しい Solana プログラムの構築
- 02トークン、ボールト、エスクローの実装
- 03独自のアカウント検証と CPI ロジックの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sendaifun/ skills --skill pinocchio-developmentインストール後、Claude Code で/pinocchio-developmentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
docs/edge-cases.mdmigration-from-anchor.mdexamples/counter/README.mdexamples/token-operations/README.mdexamples/vault/README.mdresources/account-patterns.mdcpi-reference.mdoptimization-checklist.mdscripts/scaffold-program.shtemplates/program-template.rs詳細
Pinocchio 開発ガイド
Pinocchio を使って、驚異的な速さの Solana プログラムを構築しましょう。Pinocchio は、従来の方法と比べてコンピュートユニットを88~95%削減し、バイナリサイズを40%縮小する、依存関係ゼロのゼロコピー・フレームワークです。
概要
Pinocchio は、重量級の solana-program クレートを使わずに Solana プログラムを記述できる、Anza の最小構成の Rust ライブラリです。トランザクションから受け取るデータを単一のバイトスライスとして扱い、ゼロコピー技術によってその場で読み取ります。
パフォーマンス比較
| 指標 | Anchor | ネイティブ(solana-program) | Pinocchio |
|---|---|---|---|
| トークン転送の CU | ~6,000 | ~4,500 | ~600-800 |
| バイナリサイズ | 大 | 中 | 小(-40%) |
| ヒープ割り当て | 必須 | 必須 | 任意 |
| 依存関係 | 多数 | 複数 | なし* |
*オンチェーン実行用の Solana SDK 型のみ
Pinocchio を使う場面
次の場合は Pinocchio を使用してください:
- 高スループットのプログラム(DEX、オーダーブック、ゲーム)を構築する場合
- コンピュートユニットがボトルネックになっている場合
- バイナリサイズが重要な場合(プログラムのデプロイコスト)
- メモリを最大限に制御する必要がある場合
- インフラ(トークン、ボールト、エスクロー)を構築する場合
次の場合は Anchor の使用を検討してください:
- 迅速なプロトタイピング/MVP
- チームが低レベルの Rust に不慣れな場合
- アカウント間の関係が複雑な場合
- 豊富なエコシステムのツールが必要な場合
- 監査のスケジュールが厳しい場合(Anchor を知る監査人のほうが多いため)
クイックスタート
1. プロジェクトのセットアップ
# 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. 基本的なプログラム構造
use pinocchio::{
account_info::AccountInfo,
entrypoint,
program_error::ProgramError,
pubkey::Pubkey,
ProgramResult,
};
// Declare entrypoint
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Route instructions by discriminator (first byte)
match instruction_data.first() {
Some(0) => initialize(accounts, &instruction_data[1..]),
Some(1) => execute(accounts, &instruction_data[1..]),
_ => Err(ProgramError::InvalidInstructionData),
}
}
3. Bytemuck を使ったアカウントの定義
use bytemuck::{Pod, Zeroable};
// Single-byte discriminator for account type
pub const VAULT_DISCRIMINATOR: u8 = 1;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vault {
pub discriminator: u8,
pub owner: [u8; 32], // Pubkey as bytes
pub balance: u64,
pub bump: u8,
pub _padding: [u8; 6], // Align to 8 bytes
}
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]))
}
}
インストラクション
ステップ 1:アカウント検証を定義する
検証済みアカウントを保持する構造体を作成します。
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);
};
// Validate owner is signer
if !owner.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
// Validate system program
if system_program.key() != &pinocchio_system::ID {
return Err(ProgramError::IncorrectProgramId);
}
Ok(Self {
vault,
owner,
system_program,
})
}
}
ステップ 2:インストラクションハンドラーを実装する
use pinocchio_system::instructions::CreateAccount;
pub fn initialize(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let ctx = InitializeAccounts::parse(accounts)?;
// Derive PDA
let (pda, bump) = Pubkey::find_program_address(
&[b"vault", ctx.owner.key().as_ref()],
&crate::ID,
);
// Verify PDA matches
if ctx.vault.key() != &pda {
return Err(ProgramError::InvalidSeeds);
}
// Create account 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]]])?;
// Initialize account data
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(())
}
エントリーポイントの選択肢
Pinocchio には、トレードオフの異なる3つのエントリーポイントマクロがあります。
1. 標準エントリーポイント(ほとんどの場合に推奨)
use pinocchio::entrypoint;
entrypoint!(process_instruction);
- ヒープアロケーターを設定します
- パニックハンドラーを構成します
- アカウントを自動的にデシリアライズします
2. 遅延エントリーポイント(単一インストラクションのプログラムに最適)
use pinocchio::lazy_entrypoint;
lazy_entrypoint!(process_instruction);
pub fn process_instruction(mut context: InstructionContext) -> ProgramResult {
// Accounts parsed on-demand
let account = context.next_account()?;
let data = context.instruction_data();
Ok(())
}
- 必要になるまで解析を遅らせます
- シンプルなプログラムで最大の CU 削減効果を得られます
- メモプログラムのベンチマークでCUを80~87%削減します
3. アロケーターなし(最大限の最適化)
use pinocchio::{entrypoint, no_allocator};
no_allocator!();
entrypoint!(process_instruction);
- ヒープを完全に無効にします
String、Vec、Boxは使用できません- サイズが静的な操作に最適です
CPI パターン
System Program CPI
use pinocchio_system::instructions::{CreateAccount, Transfer};
// Create account
CreateAccount {
from: payer,
to: new_account,
lamports: rent_lamports,
space: account_size,
owner: &program_id,
}.invoke()?;
// Transfer SOL
Transfer {
from: source,
to: destination,
lamports: amount,
}.invoke()?;
// Transfer with PDA signer
Transfer {
from: pda_account,
to: destination,
lamports: amount,
}.invoke_signed(&[&[b"vault", owner.as_ref(), &[bump]]])?;
Token Program CPI
use pinocchio_token::instructions::{Transfer, MintTo, Burn};
// Transfer tokens
Transfer {
source: from_token_account,
destination: to_token_account,
authority: owner,
amount: token_amount,
}.invoke()?;
// Mint tokens (with PDA authority)
MintTo {
mint: mint_account,
token_account: destination,
authority: mint_authority_pda,
amount: mint_amount,
}.invoke_signed(&[&[b"mint_auth", &[bump]]])?;
カスタム CPI(サードパーティプログラム)
use pinocchio::{
instruction::{AccountMeta, Instruction},
program::invoke,
};
// Build instruction manually
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])?;
アカウント検証パターン
パターン 1: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 })
}
}
// Usage
let ctx = DepositAccounts::try_from(accounts)?;
パターン 2:ビルダーパターン
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
}
}
// Usage
let owner = AccountValidator::new(&accounts[0])
.is_signer()?
.is_writable()?
.build();
パターン 3:マクロベースの検証
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)
};
}
PDA 操作
PDA の導出
use pinocchio::pubkey::Pubkey;
// Find PDA with bump
let (pda, bump) = Pubkey::find_program_address(
&[b"vault", user.key().as_ref()],
program_id,
);
// Create PDA with known bump (cheaper)
let pda = Pubkey::create_program_address(
&[b"vault", user.key().as_ref(), &[bump]],
program_id,
)?;
CPI 用の PDA 署名
// Single seed set
let signer_seeds = &[b"vault", owner.as_ref(), &[bump]];
Transfer {
from: vault_pda,
to: destination,
lamports: amount,
}.invoke_signed(&[signer_seeds])?;
// Multiple PDA signers
let signer1 = &[b"vault", owner.as_ref(), &[bump1]];
let signer2 = &[b"authority", &[bump2]];
invoke_signed(&ix, &accounts, &[signer1, signer2])?;
データのシリアライズ
Bytemuck による固定サイズデータ(推奨)
#[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],
}
// Zero-copy read
let state: &GameState = bytemuck::from_bytes(&data);
// Zero-copy write
let state: &mut GameState = bytemuck::from_bytes_mut(&mut data);
Borsh による可変サイズデータ
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Metadata {
pub name: String,
pub symbol: String,
pub uri: String,
}
// Deserialize (allocates)
let metadata = Metadata::try_from_slice(data)?;
// Serialize
let mut buffer = Vec::new();
metadata.serialize(&mut buffer)?;
手動解析(最大限の制御)
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()))
}
Shank による IDL の生成
Pinocchio は IDL を自動生成しないため、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 },
}
IDL を生成します。
shank idl -o idl.json -p src/lib.rs
ガイドライン
- インストラクションとアカウントには、必ず1バイトの識別子を使用してください
- 固定サイズデータには、Borsh より bytemuck を優先してください
- 単一インストラクションのプログラムには、
lazy_entrypoint!を使用してください - 処理の前に、すべてのアカウントを検証してください
- PDA が所有するアカウントの操作には、
invoke_signedを使用してください - 構造体を8バイト境界に揃えるため、パディングを追加してください
solana-program-testまたは Bankrun でテストしてください
このスキルに含まれるファイル
pinocchio-development/
├── SKILL.md # This file
├── scripts/
│ ├── scaffold-program.sh # Project generator
│ └── benchmark-cu.sh # CU benchmarking
├── resources/
│ ├── account-patterns.md # Validation patterns
│ ├── cpi-reference.md # CPI quick reference
│ ├── optimization-checklist.md # Performance tips
│ └── anchor-comparison.md # Side-by-side comparison
├── examples/
│ ├── counter/ # Basic counter program
│ ├── vault/ # PDA vault with deposits
│ ├── token-operations/ # Token minting/transfers
│ └── transfer-hook/ # Token-2022 hook
├── templates/
│ └── program-template.rs # Starter template
└── docs/
├── migration-from-anchor.md # Anchor migration guide
└── edge-cases.md # Gotchas and solutions
パフォーマンスベンチマーク(2025年)
最新のベンチマークは Pinocchio の効率性を示しています。
| プログラム | Anchor CU | Pinocchio CU | 削減率 |
|---|---|---|---|
| トークン転送 | ~6,000 | ~600-800 | 88-95% |
| メモプログラム | ~650 | ~108 | 83% |
| カウンター | ~800 | ~104 | 87% |
アセンブリ実装:104 CU、Pinocchio:108 CU、基本的な Anchor:649 CU
SDK のロードマップ(Anza の計画)
Anza チームは SDK v3 の計画を発表しました。
今後の改善
- 統一された基本型:Anchor と Pinocchio で再利用できる型
- 新しいシリアライズライブラリ:ゼロコピー、よりシンプルな列挙型、可変長型
- ATA プログラムの最適化:Pinocchio に最適化された Associated Token Account
- Token22 の最適化:CU 使用量を最小限に抑えた Token Extensions の完全サポート
統合の進捗
- Pinocchio の型は Solana SDK のコアに統合中です
- Anchor と Pinocchio のプログラム間の相互運用性が向上しています
注意事項
- Pinocchio は未監査です。本番環境での使用には注意してください
- 現行バージョンは 0.10.x です(最新版:
pinocchio = "0.10") - CPI ヘルパーには
pinocchio-system = "0.4"とpinocchio-token = "0.4"を使用してください pinocchio-tokenによる Token-2022 のサポートは、現在活発に開発されています- クライアントを生成するには、Shank で生成した IDL とともに Codama を使用してください
- Anza(Solana Agave クライアントの開発者)がメンテナンスしています