新着:HeliusがLight Protocolを買収
Anchor入門
ブログ/開発

Anchor入門:Solanaプログラム構築の初心者向けガイド

Developer Experience EngineerXの0xIchigoLinkedInの0xIchigoGitHubの0xIchigo
読了時間:46分

この記事をレビューしてくださったNoah、Mike、Jonas、Ryan、Prames、bl0ckpainに心から感謝します。

この記事では何を扱いますか?

Rustは、Solanaプログラム開発の共通言語とよく表現されます。しかし、Rust開発の大半でこのフレームワークが使われていることを考えると、Anchorをそう表現するほうが正確です。Anchorは、安全なSolanaプログラムを迅速に構築するために設計された、規約を重視する強力なフレームワークです。アカウントのシリアライズとデシリアライズや命令データなどに関する定型コードを削減し、不可欠なセキュリティチェックを実行し、クライアントライブラリを自動生成し、包括的なテスト環境を提供することで、開発プロセスを効率化します。

この記事では、Anchorプログラムの開発方法を解説します。Anchorのインストール、Solana Playgroundの使用、シンプルなHello, World!プログラムの作成、ビルド、デプロイを扱います。その後、IDL、マクロ、Anchorプログラムの構造、アカウント型と制約、エラー処理を確認しながら、Anchorが開発プロセスをどのように効率化するかを詳しく見ていきます。クロスプログラム呼び出しとProgram Derived Addressについても簡単に説明します。この記事では、今すぐAnchorを使い始めるために必要なことをすべて紹介します。

前提知識

この記事では、Solanaのプログラミングモデルに関する知識を前提としています。Solana上での構築が初めての場合は、以前のブログ記事Solanaプログラミングモデル:Solana上での開発入門を読むことをおすすめします。 

Rustが初めてでも心配はいりません。Anchor開発を始めるのに高度な知識は必要ありません。Anchorのドキュメントでは、開発者はRustの基礎(つまり、Rust Bookの最初の9章)を理解していれば十分だと説明されています。Rustプログラミングの重要な概念を分かりやすく学ぶには、Rustサバイバルガイドの視聴をおすすめします。Rustのメモリ、所有権、借用のルールを理解することも重要です。

学習負担を軽減するため、低レベルのプログラミング言語に不慣れな開発者には、Rustの教材では省略されがちなシステムプログラミング固有の概念を確認することをおすすめします。たとえば、変数のサイズ、ポインタ、メモリリークなどのトピックです。Rustの実用例として、Rust By Exampleと、私のさまざまなデータ構造とアルゴリズムをRustで実装したリポジトリもおすすめします。

代わりにTypeScriptを使いたいですか?PoseidonのフレームワークでTypeScriptをRustにトランスパイルし、有効なAnchorプログラムを生成するSolanaプログラムをTypeScriptで記述する方法をご覧ください。

この記事ではAnchor開発にのみ焦点を当てます。Native Rustでプログラムを開発する方法は扱わず、その知識も前提としません。また、Anchorを使ったクライアントサイド開発も扱いません。TypeScriptを介してAnchorプログラムをテストし、操作する方法については、今後の記事で解説します。

それでは、Anchorを始めましょう!

Anchorのインストール

Anchorのセットアップでは、いくつかの簡単な手順で必要なツールとパッケージをインストールします。このセクションでは、これらのツールとパッケージ(Rust、Solana Tool Suite、Yarn、Anchor Version Manager)のインストール方法を説明します。

Rustのインストール

RustはRustの公式Webサイトまたはコマンドラインからインストールできます。

コード
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Solana Tool Suiteのインストール

AnchorにはSolana Tool Suiteも必要です。macOSとLinuxでは、次のコマンドを使って最新リリース(この記事の執筆時点では1.17.16)をインストールできます。

コード
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"

Windowsユーザーは、次のコマンドでSolana Tool Suiteをインストールできます。

コード
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"

ただし、代わりにWindows Subsystem for Linux(WSL)を使用することを強くおすすめします。これにより、デュアルブートや別の仮想マシンの起動を必要とせず、Windowsマシン上でLinux環境を実行できます。この方法を選ぶ場合は、Linux向けのインストール手順(つまりcurlコマンド)を参照してください。

開発者は、v1.17.16をダウンロードしたいバージョンのリリースタグに置き換えることもできます。また、stable、beta、edgeの各チャンネル名も使用できます。インストール後、solana –-versionを実行し、目的のバージョンのsolanaがインストールされていることを確認します。

Yarnのインストール

AnchorにはYarnも必要です。Yarnは、Node.js 14.9/16.9以降のすべての公式Node.jsリリースに含まれるCorepackを使って有効化できます。ただし、現在は実験段階のためオプトイン方式です。そのため、有効化する前に**corepack enableを実行する必要があります。一部のサードパーティディストリビューターには、Corepackがデフォルトで含まれていない場合があります。その場合、corepack enableの前にnpm install -g corepack**を実行する必要があります。

AVMを使ったAnchorのインストール

Anchorのドキュメントでは、Anchor Version Manager(AVM)を介したAnchorのインストールを推奨しています。AVMを使うと、複数のanchor-cliバイナリのインストールを簡単に管理、選択できます。検証可能なビルドを生成する場合や、プログラムごとに異なるバージョンを使用する場合に必要になることがあります。Cargoを使い、cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --forceコマンドでインストールできます。その後、最新バージョンをインストールして使用します。

コード
avm install latest
avm use latest

# Verify the installation
avm --version

利用可能なanchor-cliのバージョン一覧を表示するには、avm listコマンドを使います。開発者はavm use <version>を使って特定のバージョンを使用できます。このバージョンは変更するまで使用され続けます。avm uninstall <version>コマンドを使うと、特定のバージョンをアンインストールできます。

バイナリの使用とソースからのビルドによるAnchorのインストール

Linuxでは、Anchorのバイナリをnpmパッケージ@coral-xyz/anchor-cliから入手できます。現在サポートされているのはx86_64 Linuxのみです。そのため、ほかのオペレーティングシステムではソースからビルドする必要があります。開発者はCargoを使ってCLIを直接インストールできます。次に例を示します。

コード
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked

別のAnchorバージョンをインストールするには、--tag引数を変更します。Cargoでのインストールに失敗した場合は、追加の依存関係のインストールが必要になることがあります。たとえば、Ubuntuでは次のようにします。

コード
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev

その後、開発者はanchor --versionコマンドでAnchorのインストールを確認できます。

Solana Playground

別の方法として、開発者はSolana Playground(Solpg)を使ってAnchorを始めることもできます。Solana Playgroundは、Solanaプログラムをすばやく開発、テスト、デプロイできるブラウザベースのIDEです。 

Solana Playgroundを初めて使用する際は、Playground Walletを作成する必要があります。画面左下にあるNot connectedと表示された赤いステータスインジケーターをクリックします。次のモーダルが表示されます。

Continueをクリックする前に、バックアップとしてウォレットのキーペアファイルを保存することをおすすめします。Playground Walletはブラウザのローカルストレージに保存されるためです。ブラウザのキャッシュを消去するとウォレットも削除されます。 

Continueをクリックすると、IDEですぐに使用できるdevnetウォレットが作成されます。

ウォレットに資金を追加するには、Playgroundのターミナルでsolana airdrop <amount>コマンドを実行します。<amount>は、希望するdevnet SOLの数量に置き換えてください。または、devnet SOLを入手できるこのフォーセットにアクセスしてください。こちらのdevnet SOLの入手方法に関するガイドもおすすめします。

次のエラーが発生する場合があります。

コード
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds

多くの場合、devnetのフォーセットが枯渇しているか、要求したSOLが多すぎることが原因です。現在の上限は5 SOLで、このプログラムのデプロイには十分です。そのため、フォーセットから5 SOLを要求するか、solana airdrop 5コマンドを実行することをおすすめします。少量を何度も要求すると、レート制限が適用される可能性があります。

Hello, World!

Hello, World!プログラムは、新しいフレームワークやプログラミング言語への優れた入門とされています。シンプルで、あらゆるスキルレベルの開発者が理解できるためです。また、複雑なロジックや関数を導入せずに、新しいプログラミングモデルの基本構造と構文を明確に示せます。コーディング初心者向けの定番プログラムとなっているため、Anchorでも作成するのが自然でしょう。このセクションでは、ローカルのAnchor環境とSolana Playgroundの両方でHello, World!プログラムをビルドし、デプロイする方法を説明します。

ローカルのAnchor環境で新しいプロジェクトを作成する

Anchorがインストールされていれば、次のコマンドだけで新しいプロジェクトを作成できます。

コード
anchor init hello-world
cd hello-world

これらのコマンドは、hello-worldという新しいAnchorプロジェクトを初期化し、そのディレクトリに移動します。このディレクトリ内で、hello-world/programs/hello-world/src/lib.rsに移動してください。このファイルには、次のスターターコードが含まれています。

コード
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
pub mod hello-world {
    use super::*;

    pub fn initialize(ctx: Context) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

Anchorによって、さまざまなファイルとディレクトリが用意されています。具体的には次のとおりです。

  • プログラムのクライアント用の空の app
  • すべてのSolanaプログラムを格納する programs フォルダ
  • JavaScriptテスト用の tests フォルダ。スターターコード用に自動生成されたテストファイルが含まれています
  • Anchor.toml 設定ファイル。Rustが初めての場合、TOMLファイルは、そのセマンティクスによって読みやすく設計された最小限の設定ファイル形式です。 Anchor.toml ファイルは、Anchorがプログラムとどのようにやり取りするかを設定するために使います。たとえば、プログラムをどのクラスターにデプロイするかを指定します。

Solana Playgroundで新しいプロジェクトを作成する

Solana Playgroundで新しいプロジェクトを作成するのは非常に簡単です。左上隅に移動し、Create a New Projectをクリックします。

次のモーダルが表示されます。

プログラムに名前を付け、Anchor(Rust)を選択してCreateをクリックします。これにより、新しいAnchorプロジェクトがブラウザ内に直接作成されます。左側のProgramセクションにsrcディレクトリが表示されます。この中のlib.rsには、次のスターターコードが含まれています。

コード
use anchor_lang::prelude::*;

// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");

#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data); // Message will show up in the tx logs
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // We must specify the space in order to initialize an account.
    // First 8 bytes are default account discriminator,
    // next 8 bytes come from NewAccount.data being type u64.
    // (u64 = 64 bits unsigned integer = 8 bytes)
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct NewAccount {
    data: u64
}

Solana Playgroundでは、client.tsファイルとanchor.test.tsファイルのみが生成される点に注目してください。新しいAnchorプロジェクトで通常生成される内容の詳細については、ローカル環境でAnchorを使ってプログラムを作成するセクションを読むことをおすすめします。

Hello, World!を記述する

Anchorをローカルで使用する場合でも、Solana Playground経由で使用する場合でも、非常にシンプルなHello, World!プログラムを作るには、スターターコードを次のコードに置き換えます。

コード
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
mod hello_world {
use super::*;

pub fn hello(_ctx: Context<Hello>) -> Result<()> {
	msg!("Hello, World!");
	Ok(())
}

#[derive(Accounts)]
pub struct Hello {}
}

各部分の詳しい内容は、後続のセクションで説明します。ここでは、開発プロセスを簡素化するためにマクロとトレイトが使われている点が重要です。declare_id!マクロは、プログラムの公開鍵を設定します。ローカル開発では、プログラムをセットアップするanchor initコマンドによってtarget/deployディレクトリにキーペアが生成され、このマクロに値が設定されます。Solana Playgroundでも自動的に実行されます。

メインのhello_worldモジュールでは、**Hello, World!**をログに記録する関数を作成します。また、プログラムが正常に実行されたことを示すためにOk(())を返します。コンソールで未使用変数の警告が表示されないように、ctxの先頭にアンダースコアを付けている点に注目してください。Helloはアカウント構造体ですが、プログラムは新しいメッセージをログに記録するだけなので、アカウントを渡す必要はありません。

これだけです。アカウントを受け取る必要も、複雑なロジックを実装する必要もありません。上記のコードで、Hello, World!をログに記録するプログラムが作成されます。

ローカルでのビルドとデプロイ

このセクションでは、Localhostへのデプロイに焦点を当てます。Solana Playgroundのデフォルトはdevnetですが、ローカル開発環境のほうが開発者体験は大幅に向上します。高速なだけでなく、devnetでのテスト時によく発生する問題も回避できます。たとえば、トランザクション用のSOL不足、デプロイの遅延、devnet停止中にテストできないといった問題です。一方、ローカル開発では、テストごとに新しい状態を確実に用意できます。これにより、より制御しやすく効率的な開発環境を実現できます。

ツールの設定

まず、Localhost開発向けにSolana Tool Suiteが正しく設定されていることを確認します。solana config set --url localhostコマンドを実行し、すべての設定がLocalhostのURLを参照していることを確認してください。 

また、ローカルでSolanaとやり取りするためのローカルキーペアがあることも確認してください。Solana CLIでプログラムをデプロイするには、SOL残高のあるSolanaウォレットが必要です。solana addressコマンドを実行し、ローカルキーペアがすでにあるか確認します。エラーが発生した場合は、solana-keygen newコマンドを実行してください。デフォルトでは、~/.config/solana/id.jsonパスに新しいファイルシステムウォレットが作成されます。公開鍵と秘密鍵の復元に使用できるリカバリーフレーズも表示されます。ローカルでの使用であっても、このキーペアを保存しておくことをおすすめします。また、デフォルトの場所にファイルシステムウォレットがすでに保存されている場合、--forceコマンドで明示しない限り、solana-keygen newコマンドで上書きされることはありません。

Anchor.tomlの設定

次に、Anchor.tomlファイルがLocalhostを正しく参照していることを確認します。次のコードが含まれていることを確認してください。

コード
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'

ここで[programs.localnet]は、localnet(つまりLocalhost)上のプログラムIDを指します。プログラムIDは常にクラスターとの関連で指定されます。同じプログラムを別のクラスターでは異なるアドレスにデプロイできるためです。開発者体験の観点では、異なるクラスターにデプロイするプログラムごとに新しいプログラムIDを宣言するのは手間になる場合があります。 

プログラムIDは公開されています。一方、そのキーペアはtarget/deployフォルダに保存されます。ファイル名はプログラム名に基づく特定の命名規則に従います。たとえば、プログラム名がhello_worldの場合、Anchorはtarget/deploy/hello-world-keypair.jsonにあるキーペアを探します。デプロイ時にこのファイルが見つからない場合、Anchorは新しいキーペアを生成します。その結果、新しいプログラムIDが割り当てられます。そのため、初回デプロイ後にプログラムIDを更新することが重要です。hello-world-keypair.jsonファイルは、プログラムの所有権を証明します。キーペアが漏洩すると、悪意のある人物がプログラムを不正に変更できるようになります。 

[provider]では、ストレージとトランザクションの支払いにLocalhostと指定されたウォレットを使うようAnchorに指示しています。

ローカル台帳のビルド、デプロイ、実行

anchor buildコマンドを使ってプログラムをビルドします。名前を指定して特定のプログラムをビルドするには、anchor build -p <program name>コマンドを使い、<program name>をプログラム名に置き換えます。localnet上で開発しているため、Anchor CLIのlocalnetコマンドを使って開発プロセスを効率化できます。たとえば、anchor localnet --skip-buildは、ワークスペース内のプログラムのビルドをスキップする場合に特に便利です。テストを実行する際、プログラムのコードが変更されていなければ時間を節約できます。

ここでanchor deployコマンドを実行すると、エラーが返されます。テスト対象となるSolanaクラスターが自分のマシン上で動作していないためです。ローカル台帳を実行すれば、マシン上でクラスターをシミュレートできます。Solana CLIには、テストバリデータが組み込まれています。solana-test-validatorコマンドを実行すると、ワークステーション上で全機能を備えた単一ノードクラスターが起動します。RPCのレート制限がない、エアドロップの上限がない、オンチェーンプログラムを直接デプロイできる、ファイルからアカウントを読み込める、公開クラスターからアカウントを複製できるなど、多くの利点があります。localhostクラスターをオンラインに保ち、操作できる状態にするには、テストバリデータを別のターミナルウィンドウで実行し続ける必要があります。 

これでanchor deployを正常に実行し、プログラムをローカル台帳へデプロイできます。ローカル台帳に送信されたデータは、現在の作業ディレクトリに生成されるtest-ledgerフォルダに保存されます。このフォルダがリポジトリにコミットされないよう、.gitignoreファイルに追加することをおすすめします。また、ローカル台帳を終了しても(つまり、ターミナルでCtrl + Cを押しても)、クラスターに送信されたデータは削除されません。削除するには、test-ledgerフォルダを削除するか、solana-test-validator --resetを実行します。

おめでとうございます!初めてのSolanaプログラムをLocalhostにデプロイできました!

Solana Explorer

開発者は、ローカル台帳でSolana Explorerを使用するよう設定することもできます。Solana Explorerに移動します。ナビゲーションバーで、現在のクラスターが表示されている緑色のボタンをクリックします。

クラスターを選択できるサイドバーが開きます。Custom RPC URLをクリックします。http://localhost:8899が自動入力されます。入力されない場合は、Explorerがマシンのポート8899を参照するように入力してください。

これは、次のような理由から非常に役立ちます。

  • 開発者は、devnetやmainnetを分析するブロックエクスプローラーと同様に、ローカル台帳上のトランザクションをリアルタイムで確認できます
  • 実際のクラスター上で動作しているかのように、アカウント、トークン、プログラムの状態を簡単に可視化できます
  • エラーとトランザクション失敗に関する詳細情報を確認できます
  • 使い慣れたインターフェースにより、クラスター間で一貫した開発者体験が得られます

Devnetへのデプロイ

Localhostでの開発を推奨していますが、特にdevnetクラスター上でテストしたい場合は、devnetにデプロイすることもできます。プロセスはほぼ同じですが、ローカル台帳を実行する必要はありません。すでに操作可能な完全なSolanaクラスターが存在するためです。

solana config set --url devnetコマンドを実行し、選択されているクラスターをdevnetに変更します。これ以降、ターミナルで実行するsolanaコマンドはすべてdevnet上で実行されます。次に、Anchor.tomlファイル内の[programs.localnet]セクションを複製し、[programs.devnet]に名前を変更します。また、[provider]がdevnetを参照するように変更します。

コード
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"

[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'

プログラムをデプロイするには、devnet SOLが必要です。solana airdrop <amount>コマンドを使い、~/.config/solana/id.jsonにあるデフォルトのキーペアへエアドロップします。solana aidrop <amount> <wallet address>を使ってウォレットアドレスを指定することもできます。または、devnet SOLを入手できるこのフォーセットにアクセスしてください。こちらのdevnet SOLの入手方法に関するガイドもおすすめします。

多くの場合、devnetのフォーセットが枯渇しているか、一度に要求したSOLが多すぎることが原因です。現在の上限は5 SOLで、このプログラムのデプロイには十分です。そのため、フォーセットから5 SOLを要求するか、solana airdrop 5コマンドを実行することをおすすめします。少量を何度も要求すると、レート制限が適用される可能性があります。

次のコマンドを使って、プログラムをビルドしてデプロイします。

コード
anchor build
anchor deploy

おめでとうございます!ローカル環境から初めてのSolanaプログラムをdevnetにデプロイできました!

Solana Playgroundでのビルドとデプロイ

Solana Playgroundで、左側のサイドバーにあるToolsアイコンに移動します。Buildをクリックします。コンソールには次の内容が表示されます。

コード
Building...
Build successful. Completed in 2.20s..

declare_id!マクロ内のIDが上書きされている点に注目してください。この新しいアドレスがプログラムのデプロイ先です。次に、Deployをクリックします。コンソールには次のような内容が表示されます。

コード

Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17s

おめでとうございます!Solana Playgroundを介して、初めてのSolanaプログラムをdevnetにデプロイできました!

効果的な抽象化:IDL とマクロ

Anchor は効果的な抽象化によってプログラム開発を簡素化します。つまり、複雑なブロックチェーンプログラミングの概念を抽象化し、より理解しやすく扱いやすいものにします。たとえば、Anchor はプログラムのインターフェースを定義するために、インターフェース定義言語(IDL)を採用しています。プログラムをビルドすると、Anchor はプログラムの IDL を表す JSON ファイルを生成します。基本的に、この構造はクライアント側で使用でき、プログラムの関数やデータ構造とのやり取りの方法を定義します。Anchor は、状態管理を扱うための高レベルな抽象化も提供します。開発者は Rust の構造体を使ってプログラムの状態を定義できます。これは、生のバイト配列や手動のシリアライズを扱うより直感的です。そのため、開発者は一般的な Rust データ構造と同じ方法で状態を定義でき、基盤となるシリアライズとアカウントへの保存は Anchor が処理します。

IDL をオンチェーンで公開するのも非常に簡単です。開発者は次のコマンドで IDL を公開できます。

コード
anchor idl init --filepath   --provider.cluster  --provider.wallet

指定したウォレットがプログラムの authority であり、トランザクションに必要な SOL を十分に保有していることを確認してください。これで開発者は、Orb のようなブロックエクスプローラーで IDL を確認できます。

たとえば、Orb 上の DFlow aggregator v4 IDLはこちらです。

Anchor のマクロは、最も重要な抽象化の一つ、あるいは最も重要な抽象化です。Rust におけるマクロとは、別のコードを生成するコードです。これはメタプログラミングの一種です。宣言的マクロは、Rust で最も広く使われているマクロです。開発者は macro_rules! 構文を使い、match 式に似たものを記述できます。手続き型マクロは関数に近く、コードを入力として受け取り、そのコードを処理して結果を出力します。たとえば Anchor では、#[account] マクロが Solana アカウントの制約を定義し、適用します。これにより、アカウント管理に伴う複雑さや潜在的なエラーを減らせます。Anchor のマクロを説明するには、必然的に Anchor のプログラム構造についても触れる必要があります。

Anchor のプログラム構造

Anchor のプログラム構造は、マクロとトレイトを組み合わせてボイラープレートコードを生成し、プログラムロジックを適用するよう設計されています。この設計思想は、開発プロセスを効率化し、プログラム動作の一貫性と信頼性を確保するうえで大きな役割を果たします。

use 宣言はファイルの先頭にあります。これは Rust 言語全般のセマンティクスであり、Anchor 固有のものではないことに注意してください。この宣言は、別のパスと同義のローカル名バインディングを一つ以上作成します。use 宣言により、モジュール項目を参照するために必要なパスを短縮できます。モジュールまたはブロック内に記述できます。また、self キーワードは、共通のプレフィックスと共通の親モジュールを持つパスのリストをバインドできます。たとえば、次の use 宣言はいずれも有効です。 

コード
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};

use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};

開発者が最初に出会う Anchor マクロは declare_id! です。これはプログラムのアドレス(program ID)を宣言し、すべてのやり取りがプログラムへ正しくルーティングされるようにします。開発者が初めて Anchor プログラムをビルドすると、Anchor は新しいキーペアを生成します。特に指定しない限り、このキーペアがプログラムのデプロイに使われます。キーペアの公開鍵を declare_id! マクロの program ID として指定します。

コード
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program] 属性マクロは、プログラムの命令ロジックを含むモジュールを示します。これはエントリーポイントとして機能し、プログラムが受信した命令を解釈して実行する方法を定義します。このマクロは、命令をプログラム内の適切な関数へルーティングする処理を簡素化し、プログラムのコードを整理して管理しやすくします。このモジュール内の各関数は、それぞれ独立した命令として扱われます。各関数は最初の引数として、Context 型のコンテキストパラメーター(ctx)を受け取ります。開発者は、アカウント、実行中の program ID、残りのアカウントにアクセスできます。

Context 型は次のように定義されています。

コード
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
    pub program_id: &'a Pubkey,
    pub accounts: &'b mut T,
    pub remaining_accounts: &'c [AccountInfo<'info>],
    pub bumps: T::Bumps,
}

これは、指定したプログラムに引数以外の入力を提供するために役立ちます。program_id フィールドは Pubkey 型で、現在実行中の program ID を表します。accounts はシリアライズされたアカウントを指し、remaining_accounts は渡されたもののデシリアライズも検証もされていない残りのアカウントを指します。直接使用する場合は十分に注意してください。bumps フィールドは、#[derive(Accounts)] によって生成される Bumps 型です。これは制約の検証中に見つかった bump seed を表します。アカウント制約については後のセクションで説明します。現時点では、ハンドラーが bump seed を再計算したり、引数として渡したりする必要がないよう、利便性のために提供されていることを理解しておくことが重要です。

Context はジェネリック型であることに注意してください。Rust のジェネリクスを使用すると、開発者はあらゆるデータ型に対応する柔軟で再利用可能なコードを記述できます。構造体、列挙型、関数、メソッドが扱う具体的な型を指定せずに、その型を定義できます。代わりに、通常は T と表記されるプレースホルダーを使用します。ジェネリクスはコードの重複を減らし、可読性を高めます。たとえば、ジェネリックなデータ型を保持する列挙型は次のように定義できます。

コード
enum Option<T> {
  Some(T),
  None,
}

上のコードスニペットは Option<T> 列挙型を示しています。これは、任意の型の値(つまり Some(T))または値なし(None)を格納できる標準的な Rust の列挙型です。

ここでは、Context はジェネリック型であり、T は命令に必要なアカウント、つまり開発者がデータの保存用に作成する任意の型を指定します。Context を使用する際、開発者は T を Accounts トレイトを実装する構造体として定義できます。たとえば、Context<SetData> です。開発者はドット記法を使って Context 型内のフィールドにアクセスできます。たとえば、ctx.accounts は Context 構造体の accounts フィールドにアクセスします。

前述のとおり、#[account] マクロはカスタムアカウント型を定義します。次のセクションでは、#[account(...)] を使ってアカウント型と制約を詳しく見ていきます。現時点では、Accounts 構造体が、命令で想定するアカウントと、それらのアカウントが従うべき制約を開発者が定義する場所であることが重要です。

アカウント型

Account 型は、命令がアカウントのデシリアライズ済みデータにアクセスする場合に使用します。Account 構造体は T をジェネリック型として取り、次のように定義されています。 

コード
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }

これは AccountInfo のラッパーであり、プログラムの所有権を検証し、基盤となるデータを Rust 型にデシリアライズします。プログラムの所有権について Account.info.owner == T::owner() を満たすか確認します。つまり、データの所有者が、#[account] を使用しているクレートの ID(先ほど declare_id! で作成したもの)と同じかどうかを確認します。これは、Account がラップするデータ型(=T)が Owner トレイトを実装している必要があることを意味します。#[account] 属性は、同じプログラム内で declare_id! によって宣言された crate::ID を使用し、構造体にこのトレイトを実装します。ほとんどの場合、開発者は #[account] 属性を使うだけで、必要なトレイトと実装をデータに追加できます。#[account] 属性は、次のトレイトの実装を生成します。

アカウントのシリアライズ用トレイトを実装する際、先頭の 8 バイトは一意のアカウント discriminator に割り当てられます。この discriminator は、アカウントの Rust 識別子を SHA-256 でハッシュした結果の先頭 8 バイトによって決まります。AccountDeserialize の try_deserialize を呼び出すと、この discriminator が確認されます。無効なアカウントが指定された場合は、エラーによってアカウントのデシリアライズを終了します。

開発者が Anchor 以外のプログラムとやり取りする必要がある場合もあります。その場合、#[account] を使用する代わりに独自のカスタムラッパー型を作成すれば、Account の利点をすべて活用できます。例として次のコードスニペットを見てみましょう。

コード
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

// Rest of the program

#[derive(Accounts)]
pub struct SetData<'info> {
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
    #[account(
        constraint = my_account.mint == token_account.mint,
        has_one = owner
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub owner: Signer<'info>
}

アカウント検証の大部分はアカウント制約を通じて行われます。これについては次のセクションで説明します。ここでは、受信したアカウントが token program によって所有されていることを保証するために、TokenAccount 型がどのように使われているかを確認してください。TokenAccount は token program の Account 構造体をラップし、必要な関数を追加します。これにより、Anchor はアカウントをデシリアライズでき、開発者はアカウント制約や命令関数内でそのフィールドを使用できます。

また、上のコードスニペットでは、derive マクロが構造体全体をカプセル化している点にも注目してください。これは SetData に Accounts デシリアライザーを実装し、受信したアカウントの検証に使用されます。

アカウント検証用の構造体では、次のような複数の Account 型を使用できます。

  • Account<’info, T>:デシリアライズ時に所有権を確認するアカウントコンテナ
  • AccountInfo<’info>:型として使用できる未検証のアカウント。ただし、AccountInfo は将来のリリースで廃止される可能性が高いため、代わりに UncheckedAccount を使用する必要があります
  • AccountLoader<’info, T>:オンデマンドのゼロコピーデシリアライズを容易にする型。Account を使う場合とは異なり、開発者はアカウントの初期化後に load_init、アカウントが変更可能でない場合に load、変更可能な場合に load_mut を呼び出す必要があります
  • Box<Account<’info, T>> または Box<InterfaceAccount<’info, T>>:スタック領域を節約するための box 型。アカウントがスタックに収まらないほど大きく、スタック違反を引き起こす場合があるため、アカウントを box 化することで問題を解消できます
  • Interface<’info, T>:Program をラップし、アカウントが指定されたプログラム群のいずれかであることを検証する型。想定されるプログラムにアカウントのキーが含まれているか、またアカウントが実行可能かを確認します
  • InterfaceAccount<’info, T>:プログラムの所有権を確認し、基盤となるデータを Rust 型にデシリアライズするアカウントコンテナ
  • Option<Account<’info, T>>:任意指定のアカウントに使用する option 型
  • Program<’info, T>:アカウントが指定されたプログラムであるかを検証する型
  • Signer<’info>:アカウントがトランザクションに署名したかを検証する型
  • SystemAccount<’info>:アカウントが System Program によって所有されているかを検証する型
  • Sysvar<’info, T>:アカウントが sysvar であるかを検証する型。つまり、そのアカウントがネットワーククラスター、ブロックチェーン履歴、実行中のトランザクションに関する動的に更新されるデータを格納する特殊な型かどうかを検証します。clock、epoch_schedule、instructions、rent の各 sysvar はプログラム開発に役立ちます
  • UncheckedAccount<’info>:指定されたアカウントに対して検査が実行されないことを明示的に強調するアカウントコンテナ

アカウント制約

安全な Anchor プログラムを開発するうえで、アカウント制約は不可欠です。今後の記事では、Solana プログラムのセキュリティと Anchor プログラムのハッキングについてさらに詳しく説明します。ただし、ここでも制約について取り上げることが重要です。制約を使用すると、特定のアカウントやそこに保持されるデータが、事前定義された要件に一致するかを検証できます。#[account(...)] 属性を使用して複数の種類の制約を適用でき、ほかのデータ構造を参照することもできます。形式は次のとおりです。

コード
#[account(constraint goes here)]
pub account: AccountType

また、Accounts マクロ内では、#[instruction(...)] 属性を使って命令の引数にアクセスできる点も重要です。開発者は命令と同じ順序で命令引数を列挙する必要がありますが、必要な最後の引数より後の引数はすべて省略できます。たとえば、Anchor のドキュメントには次の例があります。 

コード
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
    ...
    Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
    ...
}

アカウント制約は、通常の制約と SPL 制約に分けられます。この記事の残りの部分では、個別の制約を説明します。これらの例では、<expr> は、想定される型の値に評価される限り、渡すことができる任意の式を表します。たとえば、owner = token_program.key() です。

プログラムの制約を分析する

使用できる制約のより包括的な一覧については、アカウントに関する Anchor のドキュメントを確認することをおすすめします。すべての制約を順に取り上げ、表のような形式で正式な定義を示すのは非常に煩雑です。ここでは、次のプログラムを分析し、実際のアカウント制約の感覚をつかむほうが有益です。

コード
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};

declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");

pub mod errors;
pub mod instructions;
pub mod state;

pub use instructions::*;
pub use state::*;

#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
  name: "Fanout",
  project_url: "http://helium.com",
  contacts: "email:hello@helium.foundation",
  policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",

  // Optional Fields
  preferred_languages: "en",
  source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
  source_revision: default_env!("GITHUB_SHA", ""),
  source_release: default_env!("GITHUB_REF_NAME", ""),
  auditors: "Sec3"
}

#[program]
pub mod fanout {
  use super::*;

  pub fn initialize_fanout_v0(
    ctx: Context<InitializeFanoutV0>,
    args: InitializeFanoutArgsV0,
  ) -> Result<()> {
    instructions::initialize_fanout_v0::handler(ctx, args)
  }

  pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
    instructions::stake_v0::handler(ctx, args)
  }

  pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
    instructions::unstake_v0::handler(ctx)
  }

  pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
    instructions::distribute_v0::handler(ctx)
  }
}

これは Helium の Fanout プログラムです。保有量に基づいてトークン保有者へ比例配分する、比較的複雑なプログラムです。現時点では制約がないため、このプロジェクトはあまり参考にならないように見えます。しかし、stake_v0 命令の StakeV0 構造体を分析すると、検討できる多数の制約があります。

mut

この命令の最初の制約は mut アカウント制約です。mut は #[account(mut)] または #[account(mut @ <custom_error>)] として定義され、@ 記法によるカスタムエラーをサポートします。この制約は、指定されたアカウントが変更可能かどうかを確認し、Anchor が状態の変更を永続化できるようにします。Helium のプログラムでは、この制約により payer アカウントが変更可能であることを保証します。

コード
...
pub struct StakeV0<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub staker: Signer<'info>,
  /// CHECK: Just needed to receive nft
  pub recipient: AccountInfo<'info>,
...

has_one

has_one 制約は #[account(has_one = <target_account)] または #[account(has_one = <target_account> @ <custom_error>)] として定義されています。target_account フィールドを確認し、アカウントが Accounts 構造体の target_account フィールドのキーと一致するかを検証します。@ アノテーションによるカスタムエラーもサポートします。

StakeV0 構造体のコンテキストでは、has_one 制約を使用し、アカウントに membership_mint、token_account、membership_collection があるかを確認します。

コード
...
#[account(
  mut,
  has_one = membership_mint,
  has_one = token_account,
  has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...

複数の has_one 制約があり、mut 制約も使用されていることに注目してください。アカウント制約では、一つのアカウントに複数の制約を同時に使用できます。

seeds, bump

seeds と bump の制約は、指定されたアカウントが、現在実行中のプログラム、seed、指定されている場合は bump から導出された PDA であることを確認するために使用します。

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

bump が指定されていない場合、Anchor は canonical bump を使用します。Seeds::program = <expr> を使用すると、現在実行中のプログラムとは異なるプログラムから PDA を導出できます。

Helium の fanout プログラムでは、seeds 制約によって、テキスト「metadata」、token_metadata_program キー、membership_collection キー、テキスト「edition」が、この PDA の導出に使われる seed かどうかを確認します。seeds::program 制約は、現在のプログラムではなく token_metadata_program が PDA の導出に使われることを保証します。

コード
...
#[account(
  mut,
  seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
  seeds::program = token_metadata_program.key(),
  bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...

token::mint, token::authority

token::mint と token::authority の制約は次のように定義されています。

  • #[account(token::mint = <target account>, token::authority = <target account>)]
  • #[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]

mint と authority のトークン制約は、TokenAccount の mint アドレスと authority を検証するために使用します。これらの制約は、検査として使用することも、init 制約と併用して、指定された mint アドレスと authority を持つトークンアカウントを作成することもできます。検査として使用する場合は、制約の一部だけを指定できます。 

Helium のプログラムでは、これらの制約を使って associated_token の mint が membership_mint と等しいか、またトークンの authority が staker に設定されているかを確認します。

コード
...
#[account(
  mut,
  associated_token::mint = membership_mint,
  associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...

init, payer, space

ここではコードを少し先に進み、init、payer、space の制約を分析します。init 制約は [#account(init, payer = <target_account>, space = <num_bytes>)] として定義されています。この制約は、System Program への CPI を通じてアカウントを作成し、そのアカウントの discriminator を設定して初期化します。これによりアカウントは変更可能としてマークされ、mut とは同時に使用できません。10 キビバイトを超えるアカウントには、#[account(zero)] を使用します。

init 制約は、いくつかの追加制約と併用する必要があります。アカウント作成の費用を支払うアカウントを指定する payer 制約が必要です。また、System Program が構造体内に存在し、system_program という名前である必要があります。space 制約も定義する必要があります。「アカウント領域」セクションでは、この制約と領域要件についてさらに詳しく説明します。

Helium の fanout プログラムでは、init コマンドが新しいアカウントを作成します。payer には、構造体内で先に pub payer: Signer<'info> として定義された payer が設定されています。アカウント領域は、FanoutVoucherV0 のサイズに、discriminator 用の 8 バイトと追加領域の 61 バイトを加えた値に設定されています。

コード
...
#[account(
  init,
  payer = payer,
  space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
  seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
  bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...

init_if_needed

init_if_needed 制約は #[account(init_if_nedded, payer = <target_Account>)] または #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)] として定義されています。この制約は init とまったく同じ機能を持ちます。ただし、アカウントがまだ存在しない場合にのみ実行されます。アカウントが存在する場合でも、init_if_needed は、正しい領域が割り当てられていることや、PDA の場合に正しい seed が設定されていることなど、すべての初期化制約が満たされているかを検証します。

init_if_needed には潜在的なリスクがあるため、feature flag で制限されています。使用する際は注意が必要です。有効にするには、init-if-needed cargo feature を指定して anchor-lang をインポートします。init_if_needed を使用する際は、再初期化攻撃への対策が不可欠です。開発者は、意図した動作でない限り、初期化後にアカウントが初期状態へリセットされないよう、コードに検査を含める必要があります。このような攻撃を軽減するには、命令の実行パスを単純に保つことがベストプラクティスです。命令を、初期化用の一つと、その後の操作用に分割することを検討してください。

Helium の Fanout プログラムは init_if_needed 制約を使用し、アカウントがまだ存在しない場合に recipient_account を初期化します。

コード
...
#[account(
  init_if_needed,
  payer = payer,
  associated_token::mint = mint,
  associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...

constraint

constraint 制約は #[account(constraint = <expr>)] または #[account(constraint = <expr> @ <custom_error>)] として定義されています。指定された式が true と評価されるかを確認します。意図したユースケースに合うほかの制約がない場合に役立ちます。また、@ アノテーションによるカスタムエラーもサポートします。

Fanout プログラムは constraint を使用し、mint の supply がゼロに設定されているかを確認します。

コード
...
#[account(
  mut,
  constraint = mint.supply == 0,
  mint::decimals = 0,
  mint::authority = voucher,
  mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...

mint::authority, mint::decimals, mint::freeze_authority 

上のコードスニペットでは、mint::decimals、mint::authority、mint::freeze_authority の制約を使用し、mint の decimals がゼロに設定されているか、voucher が authority と freeze authority を持つかを確認しています。

参考として、mint::authority、mint::decimals、mint::freeze_authority の制約は次のように定義されています。

  • #[account(mint::authority = <target account>, mint::decimals = <expr>)]
  • #[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]

これらの制約の意味は明確です。つまり、それぞれトークンの authority、decimals、freeze authority を確認します。検査として使用することも、init と併用し、指定された mint decimals と mint authority を持つ mint アカウントを作成することもできます。init と併用する場合、freeze authority は完全に任意です。検査として使用する場合は、これらの制約の一部だけを指定できます。

アカウント領域 

Solana上でプログラムが使用するすべてのアカウントは、ストレージ領域を明示的に割り当てる必要があります。この割り当ては効率的なリソース管理に不可欠であり、必要なデータだけがオンチェーンに保存されるようにします。また、トランザクションコストを予測しやすくし、トランザクション実行の効率も高めます。アカウントストレージを動的に割り当てたり、サイズ変更したりすることなくトランザクションを処理できます。さらに、データ領域を事前に割り当てることで、必要なすべてのデータを保存するのに十分な領域をアカウントに確保できます。これにより、トランザクションの失敗や潜在的なセキュリティ脆弱性のリスクを軽減できます。

変数のサイズ設定

データ型ごとに必要な領域は異なります。以下は、必要な領域を見積もるための簡単なガイドです。

  • 基本型:bool、u8、i8、u16、i16、u32、i32、u64、i64、u128、i128などの単純なデータ型は、すべて固定サイズです。サイズは、boolの1バイト(実際に使用するのは1ビットのみ)から、u128 / i128の16バイトまでです
  • 配列:配列[T;amount]の領域は、Tのサイズに要素数を掛けて計算します(つまり、amount))。たとえば、16個のu16を含む配列には32バイトが必要です
  • Pubkey:公開鍵はSolana上で常に32バイトを占有します
  • 動的型:StringとVec<T>は、慎重に検討する必要があります。どちらも長さの保存に4バイトが必要で、さらに実際の内容を保存する領域も必要です。想定される最大サイズに十分な領域を割り当てることが重要です。Stringの場合、4バイトに、Stringのバイト単位の長さを加えます。Vec<T>の場合、4バイトに、指定された型の領域と想定要素数の積を加えます(つまり、4 + space(T) * amount)
  • OptionとEnum:Option<T>型には、1バイトに加えて型Tの領域が必要です。Enumには、enum discriminator用の1バイトに加えて、最大のvariantに必要な領域が必要です
  • 浮動小数点数:f32やf64などの型は、それぞれ4バイトと8バイトを占有します。NaN値はシリアライズの失敗を引き起こす可能性があるため、注意してください

以下のガイドは、zero-copyシリアライズを使用しないアカウントにのみ適用されます。ゼロコピーシリアライズは、#[zero_copy]属性で示されます。これはメモリレイアウトにrepr(c)属性を利用し、データにアクセスするための直接的なポインターキャストを可能にします。従来のデシリアライズによるオーバーヘッドなしでオンチェーンデータを扱える効率的な方法です。#[zero_copy]は、#[derive(Copy, Clone)]、#[derive(bytemuck::Zeroable)]、#[derive(bytemuck::Pod)]、#[repr(C)]を適用するための短縮表記です。これらの属性により、アカウントを安全にバイト列として扱え、ゼロコピーデシリアライズとの互換性を確保できます。ゼロコピーデシリアライズは、非常に大きなサイズを必要とするアカウントに不可欠です。これは、ヒープやスタックの上限に達するため、BorshまたはAnchorのデフォルトのシリアライズ機構では効率的にシリアライズできないアカウントです。

Anchorの内部Discriminator

開発者は、Anchorの内部discriminator用としてspace制約に8を加える必要があります。たとえば、アカウントに32バイトが必要な場合、40バイトが必要になります。領域の計算で内部discriminatorを考慮していることを明示するため、space制約をspace = 8 + <account size>として設定することが推奨されます。  

補足すると、discriminatorは異なるデータ型を区別するために使用される一意の識別子です。これは、実行時に異なる種類のアカウントデータ構造を区別する場合に役立ちます。また、命令の先頭にも付加され、Anchorプログラム内の対応するメソッドへ命令をルーティングするために使用されます。discriminatorは、データ型の一意の識別子を表す8バイトの配列です。

初期領域の計算

アカウントの初期領域要件を計算するのは容易ではありません。InitSpaceマクロは、アカウントの構造体で使用できるINIT_SPACE定数を追加します。この定数を生成するために、構造体へ#[account]マクロを含める必要はありません。Anchorのドキュメントでは、次の例が示されています。

コード
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
  pub data: u64,
  // max_len represents the length of the structure
  #[max_len(50)]
  pub string_one: String,
  #[max_len(10, 5)]
  pub nested: Vec<Vec<u8>>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub system_program: Program<'info, System>,
  #[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
  pub data: Account<'info, ExampleAccount>,
}

この例では、ExampleAccount::INIT_SPACEがExampleAccountに必要な領域を自動的に計算します。また、領域の計算時にはAnchorの内部discriminatorも考慮されます。

プログラム領域のサイズ変更

realloc制約は、命令の開始時にプログラムアカウントの領域を調整するために使用されます。アカウントが可変であること(つまり、mut)が必要で、AccountまたはAccountLoader型に適用できます。これは#[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]として定義されます。アカウントデータの長さを増やす場合、rent免除を維持するためにrealloc::payerからプログラムアカウントへlamportが転送されます。データ長を減らす場合は、プログラムアカウントからrealloc::payerへlamportが戻されます。realloc::zero制約は、新しく割り当てられたメモリをゼロ初期化するかどうかを決定します。ゼロ初期化により、新しいメモリがクリーンになり、残存データや不要なデータがない状態になります。

realloc制約を使う代わりに、AccountInfo::reallocを手動で使用することは推奨されません。再割り当てがMAX_PERMITTED_DATA_INCREASEの上限を超えないことを保証する実行時チェックがないためです。上限を超えると、ほかのアカウントのデータを上書きする可能性があります。この制約は、1つの命令内で再割り当てが繰り返されることも確認し、防止します。

例:

コード
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
  #[account(
    mut,
    seeds = [b"data"],
    bump,
    realloc = 8 + std::mem::size_of::<()>() + 48,
    realloc::payer = payer,
    realloc::zero = false
  )]
  pub update_account: Account<'info, NewData>,
  system_program: Program<'info, System>,
}

エラー

エラー処理は、プログラム開発に不可欠な要素です。プログラムの実行を停止させる可能性があるエラーを特定し、管理するための仕組みです。コードの品質、保守性、機能性を確保するには、エラー処理を意図的に計画する必要があります。Anchorは、堅牢なエラー処理機構によってこれを簡素化します。Anchorプログラムのエラーは、AnchorErrorsとAnchor以外のエラーに分けられます。Anchor以外のエラーにはさまざまなRustエラーが含まれるため、このセクションではAnchorErrorsに焦点を当てます。Anchor以外のエラーについては、Rust Bookのエラー処理に関する章とRust By Exampleのエラー処理に関するセクションを参照することをお勧めします。

次のstructは、AnchorErrorを定義します。

コード
pub struct AnchorError {
  pub error_name: String,
  pub error_code_number: u32,
  pub error_msg: String,
  pub error_origin: Option<ErrorOrigin>,
  pub compared_values: Option<ComparedValues>,
}

これらのフィールドは比較的単純です。error_nameは、エラー名を表す文字列です。error_code_numberは、エラーの一意の識別子(つまり、32ビットの領域を使用する一意の符号なし整数)です。error_msgは、エラーを説明するメッセージです。error_originは、関連するソースファイルやアカウントなど、エラーの発生元に関する情報を提供する任意のフィールドです。compared_valuesは、エラー発生時に比較されていた値の詳細を示す任意のフィールドです。これはデバッグに非常に役立ちます。

AnchorErrorは、logメソッドを実装しています。これにはエラーの発生元や関連する値の情報が含まれ、デバッグとエラー解決に役立ちます。このメソッドは、その情報を提供するためにerror_originとcompared_valuesを使用します。

AnchorErrorは、Anchorの内部エラーとカスタムエラーにさらに分類できます。Anchorには、返される可能性がある内部エラーコードの長いリストがあります。これらの内部エラーは、ユーザーが使用することを意図したものではありません。ただし、コードと原因の対応関係を把握しておくと役立ちます。通常は、制約に違反したときにスローされます。内部エラーコードは、次の体系に従います。

  • >= 100は命令エラーコードです
  • >= 1000はIDLエラーコードです
  • >= 2000は制約エラーコードです
  • >= 3000はアカウントエラーコードです
  • >= 4100はその他のエラーコードです
  • = 5000は非推奨のエラーコードです。

カスタムエラーは、ERROR_CODE_OFFSET(つまり6000)から始まります。

開発者は、error_code属性を使用して独自のカスタムエラーを実装できます。この属性はenumに使用し、そのenumのvariantをプログラム全体でエラーとして使用できます。各variantにはメッセージを追加できます。エラーが発生した場合、クライアントはこのメッセージを表示できます。例:

コード
#[error_code]
pub enum HeliusError {
  #[msg(“This RPC provider is too good”)]
  RPCTooGood
}

err!マクロとerror!マクロを使用して、これらのエラーをスローできます。例:

コード
require!(rpc.speed > 9000, HeliusError::RPCTooGood);

複数のrequireマクロから選択できる点は重要です。これらのマクロの大部分は、公開鍵以外の値に関係します。たとえば、require_gteマクロは、公開鍵以外の1つ目の値が2つ目の値以上であるかを確認します。

コード
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

公開鍵を比較する場合には、いくつか注意点もあります。たとえば、開発者はrequire_eqではなくrequire_keys_eqを使用する必要があります。後者の方がコストが高いためです。

すべてのプログラムはProgramErrorを返します。このエラー型にはカスタムエラー番号専用のフィールドがあり、Anchorはここに内部エラーコードとカスタムエラーコードを保存します。ただし、これは単一の数値にすぎないため、それほど有用ではありません。前述したAnchorのAnchorErrorによるログの方がはるかに役立ちます。Anchorクライアントは、これらのログを解析するように設計されています。ただし、解析が難しい場合もあります。たとえば、preflightチェックを無効にして処理されたトランザクションのログを取得するのは、それほど簡単ではありません。同様に、Anchorは、AnchorErrorを標準的な方法でログに記録しないAnchor以外のプログラムやレガシープログラム向けに、フォールバック機構も採用しています。この場合、Anchorは、トランザクションが返したエラー番号がAnchorの内部エラーコードまたはプログラムのIDLで定義されたエラー番号に対応するかを確認します。一致する場合、Anchorはエラー情報を拡充し、より多くのコンテキストを提供します。また、可能な場合は常にプログラムのエラースタックを解析し、プログラムエラーの根本原因を追跡します。ProgramErrorは基盤となるエラー型であり、Anchorのログおよび解析機構によって有用性が高まり、詳細なエラー情報を提供します。

クロスプログラム呼び出し(CPI)

この記事ではこれまでにもクロスプログラム呼び出し(CPI)について触れてきたため、専用のセクションを設けるのが適切でしょう。CPIは、プログラムから別のプログラムを直接呼び出せるようにするため、Solanaのコンポーザビリティの基盤となります。これにより、Solanaエコシステムは、開発者にとって巨大で相互接続されたAPIのようなものになります。簡潔にするため、CPIに関するAnchorのドキュメントを読むことをお勧めします。puppetプログラムとpuppet masterプログラムを使用して、CPIの実用的な例が示されています。

CPIは、あるプログラムから別のプログラムへの呼び出しであり、呼び出されるプログラム内の特定の命令を対象とするものと定義できます。呼び出し元のプログラムは、呼び出されたプログラムが命令の処理を完了するまで停止します。 

権限昇格

CPIを使用すると、呼び出し元プログラムの署名者権限を呼び出し先へ拡張できます。権限の拡張は便利ですが、非常に危険になる可能性があります。CPIが誤って悪意のあるプログラムを対象にすると、そのプログラムは呼び出し元と同じ権限を取得します。Anchorは、次の2つの安全策でこのリスクを軽減します。

  • Program<’info, T>型により、指定されたアカウントが想定されるプログラム(T)と一致することを保証します
  • Program型を使用しない場合でも、自動生成されたCPI関数はcpi_program引数が想定されるプログラムに対応することを検証します

CPIの実行

プログラムは、solana_program crateのinvokeまたはinvoke_signedを使用してCPIを実行できます。Anchorは、CPIの引数以外の入力を指定するためのCpiContext構造体も提供します。

invoke

invoke関数は、PDAを署名として使用する必要がない場合に使用します。この場合、ランタイムは呼び出し元プログラムの元の署名を呼び出し先へ拡張します。この関数は次のように定義されます。

コード
pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> ProgramResult

別のプログラムを呼び出すには、プログラムID、呼び出し先プログラム向けの命令データ、および呼び出し先がアクセスするアカウントのリストを含むInstructionを作成します。プログラムは、プログラムのentrypointでのみランタイムからAccountInfo値を受け取ります。呼び出し先プログラムが呼び出しに必要とするすべてのアカウントは、呼び出し元プログラムが含めて提供する必要があります。たとえば、呼び出し先プログラムが特定のアカウントを変更する必要がある場合、呼び出し元プログラムはそのアカウントをAccountInfo値のリストに含める必要があります。これは呼び出し先のプログラムIDにも当てはまります。つまり、呼び出し元は、呼び出し先のプログラムIDを含めて、どのプログラムを呼び出すかを明示的に指定する必要があります。

Instructionは通常、呼び出し元プログラム内で構築されますが、外部出力からデシリアライズすることもできます。

呼び出し先プログラムでエラーが発生するか、処理が中止されると、トランザクション全体が直ちに失敗します。invoke関数は、成功した場合にしか戻らないためです。CPIの結果としてデータを返すには、set_return_data関数またはget_return_data関数を使用します。返される型は、AnchorSerialize traitとAnchorDeserialize traitを実装している必要があります。または、呼び出し先から専用アカウントへ書き込み、データを保存します

プログラムは自身を再帰的に呼び出せますが、別のプログラムによる間接的な再帰呼び出し(つまり、リエントランシー)が発生すると、トランザクションは直ちに失敗します。

たとえば、CPI経由でトークンを転送するプログラムがある場合、invokeを次のように使用します。

コード
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

invoke_signed

invoke_signedは、PDAを署名者として必要とするCPIに使用します。PDAの導出に必要なseedを指定することで、呼び出し元プログラムがPDAに代わって動作できるようにします。

コード
pub fn invoke_signed(
	instruction: &Instruction,
	account_infos: &[AccountInfo<'_>],
  signers_seeds: &[&[&[u8]]]
) -> ProgramResult

PDAは、CPIで署名者として動作することもできます。ランタイムは、指定されたseedと呼び出し元プログラムのprogram_idを使用し、create_program_address経由でPDAを内部的に生成します。その後、PDAが命令とともに渡されたアドレス(つまりaccount_infos)と照合され、有効な署名者であることが確認されます。

この関数では、呼び出し元プログラムが制御する1つ以上のPDAに代わって呼び出しへ署名できます。これにより、呼び出し先は、暗号学的に署名されているかのように指定されたアカウントを操作できます。signer_seedsは、PDAの導出に使用するseed sliceで構成されます。呼び出し中、ランタイムはaccount_info内の一致するアカウントを「署名済み」と見なします。たとえば、PDA用のアカウントを作成するプログラムがある場合、invoke_signedを次のように呼び出します。

コード
invoke_signed(
	&system_instruction::create_account(
  	&payer.key,
  	&vault_pda.key,
  	lamports,
  	vault_size,
  	&program_id,
  ),
  &[
  	payer.clone(),
  	vault_pda.clone(),
  ],
  &[
  	&[
  		b"vault",
  		payer.key.as_ref(),
  		&[vault_bump_seed],
  	],
  ]
)?;

CpiContext

Anchorは、invokeまたはinvoke_signedを使用する代わりに、CPIをより簡単に実行する方法としてCpiContextを提供します。この構造体はCPIに必要な引数以外の入力を指定し、Contextの機能を忠実に再現します。命令に必要なアカウント、関連する追加アカウント、呼び出すプログラムID、および必要な場合はPDAを導出するseedに関する情報を提供します。PDAを使用しないCPIにはCpiContext::newを、PDA署名者が必要なCPIにはCpiContext::new_with_signerを使用します。

CpiContextは次のように定義されます。Tは、ToAccountMetas traitとToAccountInfos<’info> traitを実装する任意のオブジェクトを包含するジェネリック型です。

コード
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
    T: ToAccountMetas + ToAccountInfos<'info>,{
    pub accounts: T,
    pub remaining_accounts: Vec>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
}

Accountsはジェネリック型であり、ToAccountMetas traitとToAccountInfos<’info> traitを実装する任意のオブジェクトを使用できます。これは#[derive(Accounts)]属性マクロによって可能になり、コードの整理と型安全性の向上に役立ちます。 

CpiContextは、AnchorプログラムとAnchor以外のプログラムの呼び出しを効率化します。Anchorプログラムの場合は、プロジェクトのCargo.tomlファイルで依存関係を宣言し、Anchorが生成したcpiモジュールを使用するだけです。

コード
[dependencies]
callee = { path = "../callee", features = ["cpi"]}

features = [“cpi”]を設定すると、プログラムはcallee::cpiモジュールにアクセスできるようになります。Anchorはこのモジュールを自動的に生成し、プログラムの命令をRust関数として公開します。この関数はCpiContextと追加の命令データを受け取ります。これはAnchorプログラムの通常の命令関数の形式を踏襲していますが、Contextの代わりにCpiContextを使用します。cpiモジュールは、命令の呼び出しに必要なアカウント構造体も提供します。

たとえば、呼び出し先プログラムにGeneralKenobi構造体で定義された特定のアカウントを必要とするhello_thereという命令がある場合は、次のように呼び出します。

コード
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;

#[program]
pub mod fight_on_utapau {
use super::*;

pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
	let cpi_accounts = GeneralKenobi {
		jedi: ctx.accounts.jedi.to_account_info(),
		// Other account infos needed for the GeneralKenobi struct go here
	};

	let cpi_program = ctx.accounts.jedi_program.to_account_info();
	let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

	hello_there(cpi_ctx, data);
}

#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
	pub jedi: UncheckedAccount<'info>,
	pub jedi_program: Program<'info, Jedi>,
	// Other required accounts
}

pub struct GreetingParams {
	// Params required for the hello_there function
}

fight_on_utapauモジュールでは、CpiContextを使用してCPIを実行します。call_hello_there関数は、jediプログラムとやり取りするように設計されています。この関数は、jediプログラムのGeneralKenobiアカウント構造体に必要なアカウント情報と、jediプログラムのアカウント情報を使用してCpiContextを作成します。このコンテキストでhello_thereを呼び出し、GreetingParams構造体で指定された追加の必須パラメーターを渡します。CallGeneralKenobi構造体は、この関数に必要なアカウントを定義し、処理を効率化します。

最後に、Anchor以外のプログラムの命令を呼び出す場合は、プログラムのメンテナーが、そのプログラムを呼び出すためのヘルパー関数を含む独自のcrateを公開しているか確認してください。呼び出す必要がある命令を持つプログラム向けのヘルパー関数がない場合は、invokeとinvoke_signerを使用してCPIを整理し、準備します。

プログラム派生アドレス(PDA)

PDAは曲線外にあり、対応する秘密鍵を持たないことを覚えておいてください。PDAを使用すると、プログラムが命令に署名し、開発者がオンチェーンでハッシュマップのような構造を構築できます。PDAは、任意のseedのリスト、bump seed、プログラムIDを使用して導出されます。 

繰り返しになりますが、指定されたアカウントが、現在実行中のプログラム、seed、および指定されている場合はbumpから導出されたPDAであることを確認するには、次の制約を使用します。

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

bumpが指定されていない場合、Anchorはcanonical bumpを使用します。Seeds::program = <expr>を使用すると、現在実行中のプログラムとは異なるプログラムからPDAを導出できます。

seeds制約とbump制約を使用すると、導出プロセスを効率化できます。

コード
#[derive(Accounts)]
struct ExamplePDA<'info> {
	#[account(seeds = [b"example"], bump)]
	pub example_pda: Account<'info, AccountType>,
}

ここでは、seeds制約を使用してPDAを導出します。Anchorは、命令へ渡されたアカウントがseedから導出されたPDAと一致することを自動的に検証します。特定の値を指定せずにbump制約を使用すると、Anchorはデフォルトでcanonical bumpを使用します。

Anchorでは、ほかのアカウントフィールドや命令データに基づく動的なseedも使用できます。これは、構造体内のほかのフィールドを参照するか、#[instruction(...)]属性マクロを使用してデシリアライズされた命令データを含めることで実現します。たとえば、次の構造体では、example_pdaが静的seed、命令データ、署名者の公開鍵を組み合わせて使用するように制約されています。

コード
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
	#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
 	pub example_pda: Account<'info, AccountType>,
	#[account(mut)]
	pub signoooorrr: Signer<'info>
}

まとめ

Anchorを単に強力なフレームワークと呼ぶだけでは、その実力を十分に表現できません。Anchorがさまざまなマクロやトレイトを活用してコード量を削減し、開発プロセスを効率化していることは、ここまで見てきたとおりです。充実したドキュメントに加え、関連するチュートリアルやcrateの堅牢なエコシステムにも支えられています。Anchorは、Solana開発者の大多数に支持され、利用されています。

この記事は、Anchorでプログラムを開発するための非常に、非常に包括的なガイドです。Anchorのインストール、Solana Playgroundの使用方法、Hello, World!プログラムの作成、ビルド、デプロイについて解説しました。さらに、Anchorによる効果的な抽象化の手法、一般的なAnchorプログラムの構造、利用可能な多くのアカウントタイプと制約についても詳しく見てきました。また、アカウント領域の割り当てとエラー処理の重要性も取り上げました。最後に、CPIとPDAについて解説しました。これはまさにAnchorを学ぶための決定版記事です。Solanaで今すぐプログラム開発を始めるために必要な情報がすべて揃っています。

ここまでお読みいただき、ありがとうございます!以下にメールアドレスを入力して、Solanaの最新情報を見逃さないようにしましょう。さらに深く学ぶ準備はできましたか?Discordに参加して、Anchorプログラムの開発を始めましょう。

その他のリソース

Heliusを購読

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

拡大画像