新着:HeliusがLight Protocolを買収
PinocchioでSolanaスマートコントラクトを構築
ブログ/開発

PinocchioでSolanaプログラムを構築する方法

大手Solana開発エージェンシーXのExo TechnologiesLinkedInのExo Technologies
Exo Technologies共同創業者XのTaylor JohnsonLinkedInのTaylor Johnson
読了時間:12分

Pinocchioは、ネイティブSolanaプログラムの構築に使用できる、高度に最適化された依存関係ゼロのライブラリです。Pinocchioは、SolanaのAgaveクライアントのコア開発者であるAnzaによって開発されました。 

Exo Techは、Pinocchioをいち早く導入した主要なSolana開発会社です。クライアント向けの開発を通じて、Pinocchioを使用した複数の本番プログラムを開発し、不足していた機能を追加するためにSDKにも貢献してきました。 

この記事では、Pinocchioによるプログラム構築について、メリットとトレードオフを含めて詳しく解説します。開発者がPinocchioを自身のプログラムに適しているか判断できるよう、必要な知識を提供することを目指します。ただし、Pinocchioは開発者体験より最適化を優先しているため、初心者向けではない点に注意が必要です。

Pinocchioライブラリとは?

Pinocchioライブラリは、zero-copy型を広範に活用してプログラム実行を最適化する、solana-programクレートの代替です。zero-copyでは、データの読み書き時に別のメモリアドレスへコピーする必要がないため、計算リソース(SolanaではCU)を節約できます。

このライブラリには依存関係がなく、「no_std」です。Rustのstdクレートは、オペレーティングシステムのリソースへアクセスする一般的な方法とランタイムを提供しますが、Solana Virtual Machine(SVM)自体がランタイムであるため、このオーバーヘッドは不要です。

Pinocchioがsolana-programより高性能なのはなぜですか?

すべてのSolanaプログラムには、プログラム実行時にランタイムから呼び出されるエントリーポイントが必要です。solana-programライブラリはentrypoint!マクロを公開しており、プログラム入力のデシリアライズ、ヒープアロケータのセットアップ、パニックハンドラーの作成を行います。

コード
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は3つのエントリーポイントマクロを公開しています。

solana-programから移行する場合、entrypoint!マクロは、プログラム入力をデシリアライズし、アロケータとハンドラーをセットアップするため、ほぼ同じように機能します。 

一方、残りの2つのマクロは、エントリーポイントをヒープアロケータやパニックハンドラーのセットアップから切り離します。これにより、開発者はプログラムロジックが実行される前に、それらを省略または最適化できます。 

program_entrypoint!はsolana-programと同様にプログラム入力をデシリアライズします。一方、lazy_program_entrypoint!は入力バッファをラップするだけで、処理をプログラム側に委ねるため、計算処理をより細かく制御できます。 

これらのマクロはヒープアロケータやパニックハンドラーをセットアップしないため、Pinocchioライブラリでは開発者が利用できるデフォルトのマクロを公開しています。

また、ヒープメモリが一切不要だと分かっているプログラムでは、no_allocator!を使用してメモリアロケータのセットアップを省略し、計算ユニット(CU)を節約できます。

Pinocchioのエントリーポイントは、Solanaプログラムの入力をどのように異なる方法でデシリアライズしますか?

solana-programとPinocchioのエントリーポイントがプログラム入力をデシリアライズする仕組みについて簡単に触れました。しかし、このデシリアライズ方法の違いを理解することが重要です。ここでCUを大幅に節約できるためです。 

一見すると、プログラムの命令ハンドラーに渡されるデシリアライズ済み入力は同じように見えます。

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

主な違いはAccountInfoの実装にあります。 

solana-programは、データを所有するAccountInfo構造体へデータを書き込みます。一方、PinocchioのAccountInfo構造体は、それ自体がアカウントを表す基底入力データへのポインタにすぎません。これにより、コピーが必要なデータ量が減り、多くのCUを節約できます。

Pinocchioによって開発者はどのようにCUを最適化できますか?

命令プロセッサはポインタへの参照を受け取るため、Pinocchioライブラリを使用する開発者は、処理対象のデータをロジック側が所有するケースがほとんどないことに気づくでしょう。 

これは、AccountInfoの値へアクセスするとよく分かります。key()メソッドでアカウントの公開鍵を読み取ると、Pubkeyへの参照が返されます。これにより、プログラム実行中のアカウント情報の読み取りや、アカウントデータの変更にかかるコストを抑えられます。

PinocchioのCU最適化例:P-token

ゼロコピーを継続的に活用して最適化している優れた例が、p-tokenプログラムです。

このプログラムは標準のSPL Token Programを置き換えるために作られていますが、Pinocchioを使用することで各トランザクションの計算ユニットを大幅に削減しています。

すぐに気づくのは、すべての状態へポインタ経由でアクセスしている点です。

トークンアカウントをデシリアライズする代わりに、AccountInfoのデータを検証してからポインタを返します。

各プロパティには関数を通じてアクセスし、プリミティブではないすべての値は、ゼロコピーを維持する参照を返します。 

これによってCU使用量が大幅に削減される理由については、CU最適化に関するこちらの記事をご覧ください。

PinocchioとAnchorの比較

Anchorは、Solanaプログラムの開発で非常に人気のある規約重視のフレームワークです。AccountInfoのような基盤構造を公開するロジックを含まないため、Pinocchioより高水準だと考えられています。

代わりに、Anchorは前述のsolana-programクレートに依存し、プログラム開発プロセスを効率化するトレイトとマクロを公開しています。Anchorは命令ディスクリミネータのパターンとアカウントのデシリアライズロジックを提供します。このデシリアライズロジックはBorshに依存しています。Borshはゼロコピーではないため、データを別のメモリアドレスへコピーする必要があります。 

Anchorの利便性はSolanaプログラムの開発を加速させますが、その代わりにCU使用量が増加します。

一方、Pinocchioは、計算リソースを細かく調整する必要がある場合にsolana-programを置き換えるためのライブラリです。特定の設計を強制せず、開発者が適切だと考える方法で自由にプログラムを構成できます。Anchorプロジェクトには明確に定義された構造がありますが、Pinocchioプロジェクトのレイアウトはそれぞれまったく異なる場合があります。 

Pinocchioライブラリは、クライアントバインディングやその実装を扱いません。一方、AnchorはIDL生成を標準でサポートしており、生成したIDLをクライアント側からプログラムとのやり取りに使用できます。

Pinocchioを使用する開発者は、自分で作成するか、ShankやCodamaなどの別のツールを使用する必要があります。これらについては、後述のPinocchioでの構築を補完するツールセクションで説明します。

PinocchioとSteelの比較

SteelもSolanaプログラムを作成するためのフレームワークです。現在はsolana-program上に構築されており、安全で表現力豊かなプログラムを簡単に作成できるマクロ、関数、パターンを公開しています。

Steelは規約が明確なため読みやすく、同時にモジュール性も維持しています。すべてを採用するか一切採用しないかの選択になるAnchorとは異なり、開発者はSteelの必要なコンポーネントだけを使用できます。

Steelのaccount!マクロは、アカウント構造の解析にbytemuckを使用します。一方、Pinocchioはアカウントの解析を一切扱いません。Steelにはチェーン可能なパーサーとアサーションも含まれているため、カスタム検証を簡単に追加できます。Pinocchioには、このようなパターンが標準では用意されていないため、開発者が独自の検証パターンを作成する必要があります。

ただし、System ProgramやToken Programなどへの一般的なクロスプログラム呼び出し(CPI)については、PinocchioとSteelのどちらも簡単に呼び出せるパターンを公開しています。

Pinocchioは高度に最適化されていますが、すべての詳細を開発者に委ねています。Steelは、開発者体験を向上させるために設計された、solana-programライブラリ向けの優れたモジュール式ラッパーです。

Pinocchioを使用してトークンを作成する方法

Pinocchioで作成されたプログラムの例として、Solana開発者向けサンプルのトークン作成プログラムを書き直します。

これは、Token2022のトークンミントを作成し、Metadataのトークン拡張機能を使用してトークン情報を保存する、単一命令のシンプルなプログラムです。メタデータは、名前、シンボル、uriを含む命令データとして渡されます。

1. エントリーポイントを定義する

まず、プログラムのエントリーポイントを定義します。

Pinocchioのデフォルトアロケータとパニック処理を使用するため、完全なエントリーポイントマクロを使用します。

コード
entrypoint!(process_instruction);

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

2. 命令データ構造を定義する

次に、他のサンプルプログラムと一致する命令データの構造を定義します。開発時間を短縮するため、デシリアライズにはBorshを使用します。より最適なデシリアライズ方法については、別の記事で取り上げます。

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

3. アカウントと命令データを解析する

次に、命令プロセッサ内のロジックを記述します。

最初に、アカウントリストからアカウントを分割代入し、命令データをCreateTokenArgsへデシリアライズする必要があります。

コード
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. Token2022ミントアカウントを作成する

アカウントと命令データを解析したら、System programのCreateAccount命令を呼び出します。

以下では、`pinocchio_system crate as it makes it very convenient to CPI by setting values of the struct and calling invokeのCreateAccount構造体を使用しています。

通常のSPL Tokenミントを作成する場合とは異なり、使用するトークン拡張機能に必要な追加領域を算出する必要があります。

Metadata Pointer拡張機能のサイズは固定ですが、Token Metadata拡張機能は渡された引数に基づいて動的に計算する必要があります。

コード
 /// [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()?;

CreateAccountの呼び出し後、SystemProgramはToken2022プログラムをミントAccountの所有者として登録します。

5. 拡張機能、アカウント、メタデータ値を初期化する

次に、Metadata Pointer拡張機能を初期化し、Token2022プログラムでMintアカウントを初期化して、プログラムが引数として受け取ったメタデータ値を初期化することで、アカウントデータを設定します。 

以下のCPIは、現在活発に開発されているpinocchio_tokenクレートのブランチから取得しています。Token2022の機能はSPL Tokenクレートから分離される予定のため、このコードは古くなっている可能性があります。

コード
// 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()?;

以上です! 

これで、Pinocchioを使用して作成された、Token2022による自己完結型メタデータを持つトークンミントが完成しました。

最大限に最適化するため、このコードには改善の余地があります。それでも、Pinocchioでプログラムを作成する方法を理解する助けになれば幸いです。

Pinocchioでの構築を補完するツール

Pinocchio専用のツールはまだ少ないものの、増えつつあります。

アカウントのシリアライズとデシリアライズにBytemuckを使用する

アカウントのシリアライズとデシリアライズは、Pinocchioプログラムの開発者が実装する必要があります。手作業で行うと、面倒でエラーが発生しやすいプロセスです。Bytemuckは、バイト配列を構造体として簡単に読み書きできる優れたライブラリです。メモリへコピーする必要があるデータ量を抑えるため、かなり最適化されています。

固定サイズではないアカウントを扱う場合は、Borshも選択肢の1つです。ただし、計算効率では劣り、AnchorではなくPinocchioが選ばれる理由の1つでもあります。

ShankでIDLを生成する

Pinocchioはライブラリであるため、Anchorのような組み込みのIDL生成機能はありません。IDL(Interface Definition Language)は、命令、アカウント構造、エラーコードなど、Solanaプログラムの公開インターフェースを定義するJSONファイルです。標準化されたやり取りを可能にし、クライアント側の開発を簡素化します。

IDLの生成には、Shankを推奨します。このクレートを使えば、開発者はコードへ非常に簡単にアノテーションを付け、CLIで有効なIDLを生成できます。構造体のderive宣言にShankAccountマクロを追加すると、その構造体がシリアライズおよびデシリアライズ可能なAccountであることを示せます。shank CLIを実行すると、この構造体はIDL内の型付きアカウントとなり、クライアント生成に使用できるようになります。

もう1つの重要なマクロは、プログラムの命令enumに使用するShankInstructionです。これにより、#[account]属性を使用して、その命令におけるリスト内の各アカウントのインデックスと権限を指定できます。

Anchor以外のプログラムでもIDLを簡単に生成できる便利なコードアノテーションについては、shank-macroリポジトリをご覧ください。

Codamaでクライアントを生成する

IDLを用意すれば、Codamaでクライアントを簡単に生成できます。生成されたコードが要件に合わない場合は、クライアントを手動で作成する必要があります。

Exo Techでは、Solanaプログラムのリポジトリをすばやく立ち上げられるよう、Pinocchioプロジェクトテンプレートを作成しました。ぜひお試しいただき、改善案があればプルリクエストを作成してください!

Pinocchioの今後

Pinocchioはsolana-programをそのまま置き換えられるよう設計されていますが、現時点では同等の機能を備えていません。一部のsysvarはまだサポートされておらず、非コアクレートも完全にはサポートされていないか、そもそも存在しません。たとえば、Pinocchio Tokenプログラムのクレートでは、複数の署名者がサポートされていません。Token2022も現在開発中で、まだサポートされていません。

Pinocchioを使用する際の大きな欠点の1つは、他のSolanaプログラム向けに開発されたすべてのSDKがsolana-programクレートを使用していることです。つまり、各SDKはAccountInfoや受け渡されるデータを所有する必要があり、Pinocchioで開発されたプログラムとの相互運用が非常に困難になります。 

サードパーティ製プログラムと統合する場合、命令ごとにカスタムCPIロジックを作成しなければならないことがよくあります。将来的にはCodamaのようなコードジェネレーターで解決される可能性がありますが、まだその段階には達していません。

Pinocchioは現在も活発に開発されており、監査を受けていない点に注意が必要です。コミュニティでは、残りのsysvarをSDKへ追加するとともに、TokenやToken2022などの重要なSPLプログラムへの対応改善を進めています。

Pinocchioへ貢献する方法

Pinocchioには、比較的取り組みやすい貢献の機会が数多くあります。

追加の支援が必要な未解決のissueや既存のプルリクエストがあります。議論に参加するか、メンテナーのレビュー用にプルリクエストを作成してください!

まとめ

Pinocchioは、従来のソリューションと比べてSolanaプログラムをはるかに高性能に作成できるライブラリです。プログラムのエントリーポイントに関する柔軟性を高め、プログラム入力へのアクセスにzero-copyを使用することで、開発者はCU使用量を削減できます。ただし、まだ新しいライブラリであり、機能も完全ではありません。執筆時点では監査を受けていないため、注意して使用してください。

Pinocchioを使用するかどうかを評価する際は、他のライブラリやフレームワークとのトレードオフを比較することが重要です。

Anchorのような規約重視のフレームワークは、プログラム開発を加速し、保守もしやすくなります。そのため、市場投入までのスピードが重要な場合に最適です。

製品が安定し、大量のトランザクションを処理するようになった段階では、PinocchioのようなライブラリでSolanaプログラムを最適化する方が適している場合があります。

その他のリソース

詳しくは、Solana Accelerate 2025でのFeboのプレゼンテーションを視聴し、以下の学習リソースをご覧ください。

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます

拡大画像