こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今日はSolana Playgroundのチュートリアルの1つである「Hello Solana」を進めながら、1つ1つ解説していこうと思います。
https://beta.solpg.io/tutorials/hello-solana
公式ドキュメントなども参考にしながらまとめていきます。
第1回目ということもありだいぶ詳しく説明しています。
必要なところだけかいつまんで読んでいただければと思います。
solana_program
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
};
solana_program
とは、Solanaブロックチェーン上で動作するRust言語で書かれたコントラクトでしようされる基本となるライブラリです。
Solanaプログラムのための標準ライブラリのような役割を果たし、プログラムが実行される際に必要な機能や定義を提供します。
提供する機能
提供する機能としては以下のようなものがあげられます。
プログラムエントリポイントの宣言マクロ
Solanaプログラムのスタートポイント(エントリポイント)を宣言するためのマクロが提供されます。
これにより、Solanaのランタイムがプログラムを正しく実行開始できるようになります。
マクロとは?
マクロとは、プログラミング言語において、コードの一部を自動的に生成したり、繰り返し利用するための仕組みのことを指します。
マクロを利用することで、開発者はより少ないコードでより多くの作業を自動化し、繰り返しや共通のパターンを簡単に実装できるようになります。
マクロは、コンパイル時に展開され、プログラムに組み込まれるコードの一部となります。
Rust言語におけるマクロは非常に強力で、プログラムの書き方を大きく効率化することができます。例えば、Rustのprintln!
やvec!
などの関数はマクロです。
これらはコンパイル時に特定のコードに展開され、それによってプログラムの実行時の挙動が決定されます。
マクロは以下のような場合に特に有用です。
- コードの繰り返しを避ける
- 同じコードパターンが複数の場所で必要な場合、マクロを使ってそのパターンを一箇所に定義し、必要な場所で繰り返し使用できます。
- 抽象化と再利用性の向上
- 共通の機能をマクロとして定義することで、異なるコンテキストで簡単に再利用することができます。
- 条件付きコンパイル
- 特定の条件下でのみコードを含めたり除外したりする場合、マクロを利用してコンパイル時の条件分岐を行うことができます。
Rustのマクロは強力ですが適切に使用することが重要です。
マクロの乱用はコードの可読性を下げる可能性があるため、必要な場合にのみ使用することが推奨されます。
コアデータ型
Solanaプログラム開発に必要な基本的なデータ型が定義されています。
ロギングマクロ
プログラムの実行中にログを出力するためのマクロが用意されており、デバッグや監視に役立ちます。
シリアライゼーションメソッド
データをSolanaのブロックチェーン上で送受信可能な形式に変換するための方法が提供されます。
クロスプログラムインストラクション実行の方法
他のプログラムを実行するためのインストラクションを作成し、実行するための方法が含まれています。
システムプログラムやその他のネイティブプログラムのためのプログラムIDとインストラクションコンストラクタ
Solanaの基本的な機能を利用するためのIDや関数が用意されています。
Sysvarアクセサ
Solanaのシステム変数にアクセスするための関数が含まれています。
Solanaのブロックチェーン上で実行されるプログラムは、このsolana-program
クレートを基盤として構築されます。
また、Rustの標準ライブラリもSolanaのランタイム環境に合わせて修正されて使用されています。
クレートとは?
「クレート(crate)」とは、コードのパッケージ、またはライブラリのことを指します。
Rustのプロジェクトは1つ以上のクレートで構成され、これらクレートはコンパイルされると実行可能ファイルやライブラリとなります。
クレートはRustのモジュールシステムの最上位に位置し、プロジェクトのビルド、配布、依存関係の管理における基本的な単位となります。
クレートには大きく分けて二つのタイプがあります。
- バイナリクレート(Binary Crate)
- 実行可能ファイルを生成するクレートです。
- プログラムのエントリポイント(main関数)を含み、単独で実行することを目的としています。
- プロジェクトには通常、一つのバイナリクレートが存在します。
- ライブラリクレート(Library Crate)
- 再利用可能なコードを提供するクレートで、他のプログラムやライブラリによって利用されることを意図しています。
- ライブラリクレートは、関数、型、トレイトなどを提供し、これらを他のクレートから利用できるようにすることで、コードの再利用性を高めます。
クレートタイプ
Cargo.toml
ファイルにおいて、クレートタイプは以下のように指定されることがあります。
- cdylib
- ダイナミックリンクライブラリ(
.so
,.dll
,.dylib
など)を生成する。 - これは、他の言語の実行可能ファイルやアプリケーションからRustのコードを呼び出すために使われることがあります。
- ダイナミックリンクライブラリ(
- rlib
- Rustのライブラリクレートをコンパイルするための標準フォーマット。
- 他のRustのクレートとリンクする際に使用されます。
依存関係管理
クレートはCargo(Rustのパッケージマネージャーとビルドシステム)によって管理され、Cargo.toml
ファイルに記述された情報に基づき、依存関係の解決、ビルド、テスト、ドキュメント生成などが行われます。
Cargoは、プロジェクトの依存関係を自動的にダウンロードし、コンパイルするため、開発者は依存関係管理にかかる手間を大幅に削減できます。
クレートはRustのエコシステムにおいて中心的な役割を果たし、コードのモジュール性、再利用性、共有を促進します。
Rustの公式パッケージレジストリであるcrates.io
では、さまざまなクレートが公開されており、開発者はこれらを自由に利用することができます。
参考
https://doc.rust-jp.rs/book-ja/ch07-01-packages-and-crates.html
Solanaネットワークとやり取りするオフチェーン(ブロックチェーン外で動作する)プログラムの場合、solana-program
クレートを直接使用することは少なく、通常はsolana-sdk
クレートを利用します。solana-sdk
クレートは、solana-program
からすべてのモジュールを再エクスポート(再利用可能にする)しています。
solana-program
の使用例は、Solana Program Library(SPL)で見ることができます。
SPLは、Solanaプラットフォーム上で共通的に使用されるプログラムの集まりであり、solana-program
クレートの実用的な活用方法を学ぶのに役立ちます。
Solanaプログラムの定義
通常のRustプログラムと比べて、Solanaプログラムにはいくつか独特の特性があります。
Solanaプログラムのユニークな特性
ポイント
- オンチェーンとオフチェーンの両方でコンパイルされる
- Solanaプログラムは、ブロックチェーン上(オンチェーン)で実行される用途だけでなく、ブロックチェーン外(オフチェーン)でのクライアントによるデータ型へのアクセスが必要な場合もあるため、両方の環境でコンパイルされます。
- main関数を定義しない
- 通常のRustプログラムが
main
関数を持つのに対し、Solanaプログラムはentrypoint!
マクロを使ってエントリポイントを定義します。
- 通常のRustプログラムが
cdylib
クレートタイプでコンパイルされる- Solanaランタイムによる動的読み込みを可能にするため、Solanaプログラムは
cdylib
タイプのクレートとしてコンパイルされます。
- Solanaランタイムによる動的読み込みを可能にするため、Solanaプログラムは
- 制約されたVM環境で実行される
- Solanaプログラムは制約された仮想マシン環境で実行され、Rust標準ライブラリにアクセスできますが、OSサービスに関連する多くの機能は、実行時に失敗するか、何もしないか、または定義されていません。
cdylibとは?
cdylib
は、Rustでコンパイルされるクレートタイプの1つで、C言語の動的リンクライブラリ(DLL)として利用可能なバイナリを生成します。
このタイプのクレートは、Rustで書かれたライブラリをC言語や他の言語から呼び出すために使用されることが多いです。
cdylibの特徴と用途
- 互換性
- 生成された動的リンクライブラリは、C言語のABI(Application Binary Interface)に準拠しているため、C言語を含む多くのプログラミング言語から利用することができます。
- プラットフォーム間での共有
.so
(Linux)、.dll
(Windows)、.dylib
(macOS)など、様々なプラットフォームでサポートされる形式でライブラリを提供できます。
- アプリケーションとの統合
- Rustで書かれたコンポーネントやライブラリを、既存のC言語や他の言語で書かれたアプリケーションに統合する際に便利です。
cdylibの利用シナリオ
- 外部アプリケーションのプラグイン
- Rustで安全かつ効率的なプラグインや拡張機能を開発し、それを既存のアプリケーションに動的にロードして使用する。
- 既存のシステムへの統合
- Rustの強力な型システムとパフォーマンスの利点を活用して、特定の処理を実装し、それをC言語などで書かれた既存のシステムに組み込む。
- 言語間の橋渡し
- Rustで高性能なライブラリを開発し、PythonやRubyなどの他の言語から呼び出すことで、アプリケーション全体のパフォーマンスを向上させる。
cdylib
タイプのクレートを使用する際は、Cargo.toml
ファイルに以下のように指定します。
[lib]
crate-type = ["cdylib"]
この設定により、Cargoはクレートをコンパイルする際にC言語互換の動的リンクライブラリを生成します。
これにより、Rustで書かれたライブラリが他の言語からより容易に利用できるようになります。
Solanaプログラムの骨組み
Solanaプログラムの基本的な構造は、エントリポイントを定義するモジュールと、プログラムのロジックを含む関数から構成されます。
具体的には、以下のようなコードスニペットで表されます。
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
todo!()
}
}
このコードは、Solanaプログラムのエントリポイントと、プログラムID、アカウント情報、指示データを引数として受け取るprocess_instruction
関数を定義しています。
実際のプログラムロジックはprocess_instruction
関数内で実装されます。
コードの説明
このコードは、Solanaブロックチェーン上で動作するRust言語のスマートコントラクト(またはプログラム)の基本的な骨組みを示しています。
具体的には、Solanaプログラムのエントリポイント(ブロックチェーンからプログラムが呼び出されたときに最初に実行される関数)の定義方法を示しています。
エントリポイントの定義
#[cfg(not(feature = "no-entrypoint"))]
- この属性(attribute)は、Cargoの機能フラグを使って、特定の条件下でのみモジュールをコンパイルするかを制御します。
- ここでは、
no-entrypoint
というフィーチャーが有効になっていない場合にのみ、以下のentrypoint
モジュールがコンパイルされるように指定しています。 - これは、テストや他のコンテキストでエントリポイントが不要な場合に、そのコードを除外するために使われます。
pub mod entrypoint { ... }
entrypoint
という公開モジュールを定義しています。- このモジュール内で、プログラムのエントリポイントとなる関数やロジックを定義します。
Solanaプログラムライブラリの使用
use solana_program::{...};
- Solanaプログラム開発に必要な型や関数を
solana_program
クレートからインポートしています。 - ここでインポートされているのは
AccountInfo
、entrypoint
マクロ、ProgramResult
、そしてPubkey
です。
- Solanaプログラム開発に必要な型や関数を
エントリポイント関数のセットアップ
entrypoint!(process_instruction);
entrypoint!
マクロを使用して、process_instruction
関数をプログラムのエントリポイントとして登録しています。- これにより、Solanaランタイムはプログラムが呼び出された際にこの関数を実行するようになります。
エントリポイント関数
pub fn process_instruction(...)
- 実際のプログラムロジックを含むエントリポイント関数です。
- この関数は、プログラムID、アカウント情報のリスト、および指示データを引数として受け取ります。
program_id: &Pubkey
- このプログラムの公開鍵(ID)です。
accounts: &[AccountInfo]
- トランザクションに関連するアカウントの情報を含むリストです。
instruction_data: &[u8]
- このトランザクションに対する指示(またはコマンド)をエンコードしたデータです。
-> ProgramResult
- この関数は、
ProgramResult
を返します。 ProgramResult
はResult<(), ProgramError>
のエイリアスで、プログラムの実行が成功したか、あるいは何らかのエラーが発生したかを示します。
- この関数は、
実装するべきロジック
todo!()
- この部分には、実際にトランザクションを処理するためのコードを記述します。
- 現在は
todo!()
マクロが置かれており、この部分の実装がまだ完了していないことを示しています。
このコードスニペットは、Solanaプログラムの基本的な構造を示しており、プログラム開発者この骨組みをベースとして、具体的なビジネスロジックやトランザクション処理ロジックを実装していきます。
Cargo.tomlの設定
Solanaプログラムでは、Cargo.toml
ファイルに以下の設定を含める必要があります。
[lib]
crate-type = ["cdylib", "rlib"]
[features]
no-entrypoint = []
この設定により、プログラムがcdylib
としてコンパイルされることが保証され、Solanaランタイムによる動的読み込みが可能になります。
また、rlib
クレートタイプも指定されていることで、他のRustクレートとリンクすることができます。
Cargo.tomlとは?
Cargo.toml
ファイルは、Rustプロジェクトの設定ファイルで、プロジェクトのビルドシステムとパッケージマネージャであるCargoによって使用されます。
このファイルには、プロジェクトのメタデータ(名前、バージョン、作者など)、依存関係、ビルドの設定など、プロジェクトに関する様々な情報が含まれています。Cargo.toml
ファイルの構造はTOML(Tom's Obvious, Minimal Language)形式で記述されており、読みやすく、編集しやすいのが特徴です。
主なセクション
[package]
- このセクションには、プロジェクトの名前、バージョン、作者、ライセンス情報など、プロジェクトのメタデータが含まれます。
- これらの情報は、
crates.io
などのパッケージレジストリにプロジェクトを公開する際に重要になります。
[dependencies]
- プロジェクトが依存する外部クレート(ライブラリやフレームワーク)を指定します。
- 各依存関係はクレート名とバージョン番号で指定され、Cargoはこれらの情報をもとに依存クレートをダウンロードし、プロジェクトのビルド時にリンクします。
[dev-dependencies]
- テストや例のコードなど、開発時にのみ必要な依存関係を指定します。
- これらの依存関係は、プロジェクトの本番ビルドには含まれません。
[build-dependencies]
- ビルドスクリプトの実行に必要な依存関係を指定します。
- ビルドスクリプトは、プロジェクトがコンパイルされる前に実行されるコードで、通常はプロジェクトのビルド環境を構成するために使用されます。
[lib]
- ライブラリクレートの設定を行います。
- 例えば、クレートタイプ(
crate-type
)を指定することができ、これはクレートがライブラリとしてどのようにビルドされるか(例:cdylib
,rlib
など)を制御します。
[features]
- Cargoのフィーチャー機能を使って、オプショナルな機能や構成を定義することができます。
- これにより、特定の機能を有効にしたり、コンパイル時にコードの一部をオプションとして扱ったりすることが可能になります。
例
[package]
name = "my_crate"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]
edition = "2018"
[dependencies]
serde = "1.0"
[dev-dependencies]
tokio = "0.2"
[build-dependencies]
cc = "1.0"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
この例では、プロジェクトの基本的なメタデータ、いくつかの依存関係、ライブラリクレートのタイプ、そして空のデフォルトフィーチャーが定義されています。
Cargo.toml
ファイルはプロジェクトの根幹をなす部分であり、プロジェクトのビルドや管理において中心的な役
割を果たします。
featuresとは?
Cargoのfeatures
機能は、Rustプロジェクトで条件付きのコンパイルやオプショナルな機能の管理を行うために使われます。
これにより、開発者はプロジェクトの特定の部分を有効化したり無効化したりすることができ、異なる構成や機能セットでコードをコンパイルする柔軟性を持たせることが可能です。
基本的な概念
- 機能の定義
Cargo.toml
ファイルの[features]
セクションで、プロジェクトの機能(features)を定義します。- 各機能は名前と、その機能が有効化されたときに含まれる依存関係や他の機能のリストを持ちます。
- デフォルト機能
default
という特別な機能名があり、これはプロジェクトが依存された時にデフォルトで有効化される機能のリストを指定します。- 開発者は、ユーザーが特に指定しない限り常に有効になるべき機能を
default
に含めることができます。
- オプショナル依存関係
- 依存関係は、特定の機能と関連付けられることがあり、その機能が有効化されているときにのみコンパイルに含まれます。
使用例
Cargo.toml
における機能の定義例です。
[features]
# オプショナルな機能 "gui" を定義
gui = ["serde/json", "tokio"]
# デフォルトで有効化される機能のリスト
default = ["cli"]
この例では、gui
とcli
という二つの機能が定義されています。gui
機能は、serde
のjson
フィーチャーとtokio
クレートが有効化されるときに含まれます。default
機能はcli
を含んでいますが、このcli
機能が何を指すかは示されていません。
実際には、プロジェクト内のコードで#[cfg(feature = "cli")]
のような条件付きコンパイルを使って、cli
機能が有効な場合にのみコンパイルされるコードを指定できます。
コンパイル時の機能の指定
Cargoコマンドラインツールを使用して、特定の機能を有効化または無効化することができます。
例えば、cargo build --features "gui"
コマンドを実行すると、gui
機能が有効化されてビルドが行われます。
features
によって、プロジェクトの異なるユーザーや使用状況に合わせて、コードベースを柔軟に管理することが可能になります。
例えば、いくつかの機能を実験的に提供したり、特定の環境に特化した機能を提供したりする場合に有効です。
Solanaプログラムの開発では、これらの独特な特性と構造を理解し、適切に利用することが重要です。
オンチェーンとオフチェーンのコンパイル ターゲット
Solanaプラットフォーム上で実行されるプログラムは、オンチェーン(ブロックチェーン上)とオフチェーン(ブロックチェーン外の環境)の両方で動作するように設計されています。
Solanaプログラムは、rbpf VM(eBPF命令セットの変種を実装した仮想マシン)上で実行されます。
オンチェーンとオフチェーンの実行環境は大きく異なるため、条件付きコンパイルを広範囲にわたって使用して、それぞれの環境に合わせた実装を行っています。
rbpf VMとは?
rbpf VM(Rust-based Berkeley Packet Filter Virtual Machine)は、eBPF(extended Berkeley Packet Filter)の命令セットに基づいた、Rustで実装された仮想マシンです。
eBPFはもともとLinuxカーネルでパケットフィルタリングなどの目的で使用される技術で、高度なデータパケット処理やシステムコールのフィルタリング、パフォーマンスモニタリングなどに利用されます。
eBPFは高い柔軟性とパフォーマンスを提供するため、近年ではさまざまな用途での応用が進んでいます。
rbpf VMは、このeBPFを基盤としながら、Solanaブロックチェーンプラットフォームでの使用に特化しています。
Solanaでは、スマートコントラクトやプログラムがrbpf VM上で実行され、ブロックチェーン上でのトランザクション処理やアカウント管理などを効率的に行うことができます。
rbpf VMの主な特徴
- 高性能
- rbpf VMは高速な実行速度を実現しており、Solanaのような高トランザクション速度を必要とするブロックチェーンプラットフォームに適しています。
- 安全性
- eBPFの設計はセキュリティを重視しており、不正なメモリアクセスや無限ループなどのリスクを最小限に抑える機能が備わっています。
- rbpf VMもこれらの安全性の高い特性を継承しています。
- 柔軟性
- eBPF命令セットは高度なプログラミングが可能で、複雑なビジネスロジックやデータ処理が実装できます。
- これにより、Solana上で実行されるプログラムは、多様な機能やサービスを提供することが可能です。
- プログラムの独立性
- rbpf VM上で実行されるプログラムは、他のプログラムやシステムのリソースと隔離されて動作します。
- これにより、プログラム間の干渉を防ぎ、安定した実行環境を確保しています。
Solanaプラットフォームでは、rbpf VMを通じてスマートコントラクトや各種プログラムが効率的かつ安全に実行されることで、高速なトランザクション処理と柔軟なブロックチェーンアプリケーションの開発が実現しています。
これにより、Solanaは分散型アプリケーション(dApps)や金融サービス、その他多くのブロックチェーンベースのソリューションの開発において、重要な役割を果たしています。
オンチェーンとオフチェーンの違い
オンチェーンとオフチェーン実行について
- オンチェーン実行
- プログラムがSolanaのブロックチェーン上で直接実行される状況。
- これは、スマートコントラクトやトランザクション処理など、ブロックチェーンの不変性や分散化された性質を活用する場合に行われます。
- オフチェーン実行
- プログラムがブロックチェーン外の環境で実行される状況。
- これには、ブロックチェーンネットワークとのインターフェースを提供するクライアントアプリケーションや、データの前処理や分析を行うサーバー側のアプリケーションが含まれます。
条件付きコンパイルの使用例
Solanaプログラムは、オンチェーンとオフチェーンでの実行を区別するために、Rustのcfg
属性を使用して条件付きコンパイルを行います。
具体的な例として、sol_log
関数が挙げられています。
この関数は、オンチェーンで実行される場合にはシステムコールを通じてログメッセージを出力し、オフチェーンで実行される場合にはライブラリコールを使用してログメッセージを出力します。
pub fn sol_log(message: &str) {
#[cfg(target_os = "solana")]
unsafe {
sol_log_(message.as_ptr(), message.len() as u64);
}
#[cfg(not(target_os = "solana"))]
program_stubs::sol_log(message);
}
このcfg
パターンは、オンチェーンとオフチェーンの両方で動作する必要があるユーザーコードにも適用されます。
ソラナプログラムとソラナSDK
solana-program
とsolana-sdk
は、かつては単一のクレートとして存在していましたが、異なる実行環境をサポートするために分割されました。solana-program
クレートには、オンチェーンプログラムでコンパイル時に利用できない機能や、オフチェーンシナリオで実行時に失敗するオンチェーン機能が含まれています。
SolanaのeBPF実装とその制限
オンチェーンプログラム開発ライフサイクル
Solana上でのオンチェーンプログラム開発ライフサイクルは、以下のステップに分かれます。
このプロセスは、Solanaプラットフォームでスマートコントラクトやアプリケーションを開発し、デプロイするために必要な一連の手順を提供します。
手順
- 開発環境のセットアップ
- Solana CLIツールのインストール
- Solana開発を始める最も確実な方法は、Solanaのコマンドラインインターフェイス(CLI)ツールをローカルコンピュータにインストールすることです。
- これにより、強力な開発環境を手に入れることができます。
- Solana Playgroundの利用
- 一部の開発者は、ブラウザベースのIDEであるSolana Playgroundを利用することもできます。
- これにより、ブラウザだけでプログラムの記述、ビルド、デプロイが可能になります。インストールは不要です。
- Solana CLIツールのインストール
- プログラムの記述
- Solanaプログラムの記述には、主にRust言語が使用されます。
- これらのRustプログラムは、従来のRustライブラリを作成するのと同じような方法で行われます。
- C/C++やフレームワークを使用することで、Pythonなどの言語もサポートしています。
- プログラムのコンパイル
- プログラムが記述された後、それをBerkley Packet Filter(BPF)バイトコードにコンパイルする必要があります。
- このバイトコードは、その後ブロックチェーンにデプロイされます。
- プログラムの公開アドレスの生成
- Solana CLIを使用して、開発者は新しいプログラムのためのユニークなKeypair(公開鍵/秘密鍵のペア)を生成します。
- このKeypairの公開鍵(Pubkey)は、プログラムの公開アドレス(またはprogramId)としてオンチェーンで使用されます。
- プログラムのデプロイ
- 再びCLIを使用して、コンパイルされたプログラムを選択したブロックチェーンクラスタにデプロイできます。
- これは、プログラムのバイトコードを含む多くのトランザクションを作成することで行われます。
- トランザクションのメモリサイズ制限のため、各トランザクションはプログラムの小さなチャンクを迅速にブロックチェーンに送信します。
- プログラム全体がブロックチェーンに送信された後、最後のトランザクションが送信され、バッファされたバイトコード全体をプログラムのデータアカウントに書き込みます。
- これにより、新しいプログラムが実行可能としてマークされるか、既存のプログラムのアップグレードプロセスが完了します(既に存在していた場合)。
制限事項
Solanaブロックチェーン上でプログラムを開発する時にはいくつかの制約があります。
これらの制約は、プラットフォームの安全性や効率性を確保するために設けられていますが、開発者が知っておくべき重要なポイントです。
制限事項
- Rustライブラリの制限
- Solanaのオンチェーンプログラムは、決定論的に動作し、リソースが制約されたシングルスレッド環境で実行される必要があるため、使用できるRustライブラリには制限があります。
- これは、全てのライブラリがこのような特定の実行環境に適しているわけではないためです。
- コンピュートバジェット
- ブロックチェーンの計算リソースの乱用を防ぐため、各トランザクションにはコンピュートバジェットが割り当てられています。
- このバジェットを超えると、トランザクションは失敗します。
- コールスタックの深さ
- Solanaプログラムは迅速に実行される必要があるため、プログラムのコールスタックは最大64フレームの深さに制限されています。
- 許可されたコールスタックの深さを超えると、
CallDepthExceeded
エラーが発生します。
- CPIコールの深さ
- プログラム間呼び出し(CPI)では、他のプログラムを直接呼び出すことができますが、現在は深さが4に制限されています。
- 許可されたCPIコールの深さを超えると、
CallDepth
エラーが発生します。
- 浮動小数点型のサポート
- プログラムはRustの浮動小数点演算の限定的なサブセットのみをサポートしています。
- サポートされていない浮動小数点演算を使用しようとすると、ランタイムは未解決シンボルエラーを報告します。
- 浮動小数点演算はソフトウェアライブラリを介して行われ、整数演算よりも多くのコンピュートユニットを消費します。
- 一般的には、可能な場合は固定小数点演算が推奨されます。
- 静的書き込み可能データ
- プログラム共有オブジェクトは、書き込み可能な共有データをサポートしていません。
- プログラムは、同じ共有読み取り専用コードとデータを使用して、複数の並列実行間で共有されます。
- これは、開発者がプログラムに静的書き込み可能変数やグローバル変数を含めないようにするべきことを意味します。
- 符号付き除算
- SBF命令セットは符号付き除算をサポートしていません。
- 符号付き除算命令の追加は検討中です。
これらの制約は、Solanaプラットフォームの特性や目的を理解する上で重要であり、開発者が効率的かつ安全にプログラムを設計するために考慮すべき要素です。
コアデータタイプ
Solanaブロックチェーンプラットフォームで使用される主要なデータ型について説明しています。
これらのデータ型は、Solanaのスマートコントラクト(プログラム)開発において中心的な役割を果たします。
Pubkey
Solanaアカウントのアドレス。
これは通常、ed25519公開鍵によって表されます。
ed25519公開鍵とは?
ed25519は、デジタル署名に使用される公開鍵暗号アルゴリズムの1つです。
高速な署名生成と検証、高いセキュリティレベル、および耐衝撃性(collision resistance)を特徴としています。
ed25519は、Elliptic Curve Digital Signature Algorithm(ECDSA)の一種であり、Edwards-curve Digital Signature Algorithm(EdDSA)の特定の実装を指します。
このアルゴリズムは、Curve25519という楕円曲線上で動作し、特に高速な演算が可能で、さまざまなセキュリティ脅威に対して堅牢です。
主な特徴
- セキュリティ
- ed25519は128ビットのセキュリティレベルを提供します。
- これは、現代のコンピュータ技術を使っても、十分な時間内に鍵を破ることが不可能であると考えられているレベルです。
- 高速な署名と検証
- ed25519は、署名の生成と検証が非常に高速です。
- これにより、リソースが限られている環境や、大量の署名を扱う必要がある場合でも効率的に使用できます。
- 耐衝撃性
- ed25519は耐衝撃性に優れており、同じ鍵で異なるメッセージに対して同じ署名が生成されるリスクが非常に低いです。
- 短い鍵と署名
- ed25519の公開鍵は256ビット(32バイト)、署名は512ビット(64バイト)と、非常に短いです。
- これにより、通信のオーバーヘッドを最小限に抑えることができます。
用途
ed25519は、SSH認証、TLS、暗号通貨(例:Solana、Bitcoin)など、セキュリティが要求される幅広い分野で使用されています。
特に、ブロックチェーン技術においては、トランザクションの署名やウォレットアドレスの生成などに利用されることが多いです。
Solanaとed25519
Solanaブロックチェーンでは、アカウントの識別やトランザクションの署名にed25519公開鍵を使用します。
これにより、トランザクションの安全性を保ちながら、高速な処理を実現しています。
Solanaでは、アカウントアドレスはed25519公開鍵から導出され、対応する秘密鍵によって管理されます。
これにより、ユーザーは自分の資産やデータに対するコントロールを安全に行うことができます。
アカウントを一意に識別するために使われます。
一部のアカウントアドレスは対応する秘密鍵を持っていますが、プログラムによって生成されたアドレス(Program Derived Addresses)のように、対応する秘密鍵を持たないものや、秘密鍵がプログラムの運用に関係ないものもあります。
Program Derived Addresses(PDA)については以下の記事を参考にしてください。
Hash
暗号学的ハッシュで一意性を持つデータの識別に使用されます。
ブロックを一意に識別するためや、一般的な目的のハッシュ生成に使われます。
AccountInfo
単一のSolanaアカウントの説明。
アカウントには、SOL(Solanaのネイティブトークン)、データ、所有権情報などが含まれます。
プログラムが呼び出されるとき、操作対象の全アカウントがAccountInfo
としてプログラムのエントリポイントに提供されます。
Instruction
ランタイムに対してプログラムの実行を指示するディレクティブ(指令)。
アカウントセットとプログラム固有のデータを伴います。
トランザクションの一部として、特定のプログラムを実行するために使われます。
ProgramError と ProgramResult
Solanaプログラムが返すことができるエラータイプ。ProgramResult
はResult<(), ProgramError>
のエイリアスで、プログラムの実行結果を示します。
プログラムの実行が成功したか、何らかのエラーが発生したかをランタイムに報告するために使用されます。
Sol
Solanaのネイティブトークンタイプ。
ラムポート(SOLの最小分割単位)との間で変換が可能です。
トランザクション手数料の支払い、ステーキング、その他の金融活動に使用されます。
これらのデータ型は、Solanaプラットフォーム上でのプログラム開発と実行において基本となる要素です。
プログラムはこれらの型を使用して、アカウントの管理、トランザクションの処理、エラーのハンドリングなどを行います。
シリアライゼーション
Solanaのランタイム、プログラム、およびネットワーク内では、少なくとも3種類の異なるシリアライゼーション(直列化)形式が使用されています。solana-program
クレートは、プログラムに必要なシリアライゼーション形式へのアクセスを提供します。
シリアライゼーションとは、データを一定の形式のバイト列に変換することで、通信や保存を可能にする技術です。
使用されるシリアライゼーション形式
シリアライゼーション形式
- Borsh
- NEARプロジェクトによって開発された、コンパクトで明確に定義されたシリアライゼーション形式です。
- プロトコル定義やアーカイブ保存に適しています。
- RustとJavaScriptの実装があり、すべての目的に推奨されます。
solana-program
クレートはBorshを再エクスポートしていないため、ユーザーはBorshクレートを自身でインポートする必要があります。Instruction::new_with_borsh
関数は、Borshを使用して値をシリアライズし、Instruction
を作成します。
- Bincode
- Serde Rust APIを実装するコンパクトなシリアライゼーション形式です。
- 仕様やJavaScript実装がなく、BorshよりもCPUを多く使用するため、新しいコードには推奨されません。
- システムプログラムやネイティブプログラムの命令はBincodeでシリアライズされ、ランタイムの他の目的にも使用されます。
Instruction::new_with_bincode
関数は、Bincodeを使用して値をシリアライズし、Instruction
を作成します。
- Pack
- Solana固有のシリアライゼーションAPIで、Solana Program Libraryの多くの古いプログラムによってアカウント形式の定義に使用されます。
- 実装が困難で、言語に依存しないシリアライゼーション形式を定義していないため、新しいコードには一般的に推奨されません。
new_with_borsh
pub fn new_with_borsh<T: BorshSerialize>(
program_id: Pubkey,
data: &T,
accounts: Vec<AccountMeta>
) -> Self
Solanaのプログラム開発において、特定のデータをBorshシリアライゼーション形式でエンコードして、新しい命令(Instruction
)を作成する方法に関するコードです。
Borshは、効率的なデータ構造のシリアライゼーション形式であり、Rust言語だけでなくJavaScriptでも実装されており、プロトコル定義やアーカイブストレージの使用に適しています。
Borshシリアライゼーション
Borshは、コンパクトで安定した仕様を持つシリアライゼーション形式で、特にSolanaのようなブロックチェーンプラットフォームでの使用に推奨されます。
Bincodeに比べてCPU使用量が少なく、安定した仕様とJavaScript実装を持つため、新しいコードにおいて好まれます。
new_with_borsh
関数
new_with_borsh
関数は、BorshSerialize
トレイトを実装した任意の型T
の値をBorsh形式でエンコードし、それをデータとして含む新しいInstruction
を作成します。
この関数は3つのパラメータを取ります。
ポイント
program_id
- 実行するプログラムのアドレス。
data
- Borshでシリアライズされるデータ。
accounts
- プログラムによってアクセスされる可能性のある全アカウントの記述。
使用例
#[derive(BorshSerialize, BorshDeserialize)]
pub struct MyInstruction {
pub lamports: u64,
}
pub fn create_instruction(
program_id: &Pubkey,
from: &Pubkey,
to: &Pubkey,
lamports: u64,
) -> Instruction {
let instr = MyInstruction { lamports };
Instruction::new_with_borsh(
*program_id,
&instr,
vec![
AccountMeta::new(*from, true),
AccountMeta::new(*to, false),
],
)
}
MyInstruction
という構造体が定義されています。
この構造体はBorshSerialize
とBorshDeserialize
トレイトを導出し、u64
型のlamports
フィールドを持ちます。
create_instruction
関数は、program_id
(プログラムのアドレス)、from
(送信元アドレス)、to
(宛先アドレス)、そしてlamports
(転送するラムポートの量)を受け取り、MyInstruction
のインスタンスをBorshでシリアライズしてInstruction
オブジェクトを作成します。
このInstruction
には、操作を実行するために必要なアカウントメタデータも含まれます。
このメカニズムにより、Solanaのプログラムは、自身のロジックに特化したデータ構造を効率的にブロックチェーン上で扱うことができます。
Borshシリアライゼーションの使用は、データの一貫性を保ちつつ、プログラム間でのデータの交換や保存を容易にするための強力な手段となります。
シリアライゼーションのCPUコスト
開発者は、シリアライゼーションのCPUコストと、正確性および使いやすさの必要性を慎重に検討する必要があります。
既成のシリアライゼーション形式は、手書きのアプリケーション固有の形式よりも高価ですが、アプリケーション固有の形式は正確性を保証し、複数言語の実装を提供することが難しいです。
プログラムがデータを手書きのコードでパックおよびアンパックすることは珍しくありません。
Solanaプログラムでシリアライゼーションを効果的に使用することは、データの管理とプログラム間の通信の基礎を形成します。
適切なシリアライゼーション形式を選択することは、プログラムのパフォーマンスと相互運用性に大きな影響を与えます。
クロスプログラム命令実行
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke,
pubkey::Pubkey,
system_instruction,
system_program,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
assert!(payer.is_writable);
assert!(payer.is_signer);
assert!(recipient.is_writable);
let lamports = 1000000;
invoke(
&system_instruction::transfer(payer.key, recipient.key, lamports),
&[payer.clone(), recipient.clone()],
)
}
Solanaプログラムで他のプログラムを呼び出す(クロスプログラム呼び出し、CPI)例を示しています。
この例では、あるアカウントから別のアカウントへラムポート(Solanaの基本通貨単位)を転送するプロセスを実装しています。
CPIを使用すると、Solanaプログラムは他のプログラムの機能を利用することができ、これによってプログラム間での相互作用が可能になります。
CPIの基本
invoke
とinvoke_signed
関数を使用して、他のプログラムを呼び出します。invoke
は署名が不要な呼び出しに使用され、invoke_signed
は署名が必要な場合に使用されます。
トランザクションの構築
ポイント
- アカウント情報の取得
- トランザクションを実行するために必要な
AccountInfo
(支払いを行うアカウントと受取人アカウント)を取得します。 next_account_info
関数を使用してアカウント情報を順に取得し、変数に割り当てます。
- トランザクションを実行するために必要な
- アサーションの確認
- 支払いアカウントが書き込み可能であり、署名者であること、そして受取人アカウントも書き込み可能であることを確認します。
- 転送命令の作成
system_instruction::transfer
関数を使って、支払いアカウントから受取人アカウントへ転送するラムポートの量を指定する転送命令を作成します。- この例では、転送額は100万ラムポートです。
CPIの実行
転送命令と、それに関連するアカウント情報(支払いアカウントと受取人アカウント)をinvoke
関数に渡して、命令を実行します。
この関数は、システムプログラムによるラムポートの転送を実現します。
コードの説明
先ほどのコードは、Solanaのスマートコントラクト(プログラム)内で、他のプログラム(この場合はシステムプログラム)を呼び出すためのクロスプログラム呼び出し(Cross-Program Invocation、CPI)の一例を示しています。
具体的には、あるアカウントから別のアカウントへラムポート(Solanaの基本通貨単位)を転送する操作を実行しています。
コードの解説
ポイント
- インポート
- 必要なモジュールや関数を
solana_program
クレートからインポートしています。 - これには、アカウント情報を扱うためのモジュール、プログラムエントリポイントの定義、
invoke
関数(他のプログラムを呼び出すための関数)などが含まれます。
- 必要なモジュールや関数を
- エントリポイント
entrypoint!(process_instruction);
は、このプログラムのエントリポイント(入口点)を定義しています。- これにより、Solanaランタイムはどの関数をプログラムの開始点として呼び出すべきかを知ることができます。
process_instruction
関数- この関数は、プログラムが実行されるときにSolanaランタイムから呼び出されます。関数の引数には、プログラムのID、プログラムがアクセスする必要があるアカウントのリスト、および命令データが含まれます。
- アカウント情報の取得
next_account_info
関数を使用して、引数で渡されたアカウントのリストから支払いを行うアカウント(payer
)と受取人のアカウント(recipient
)を取得しています。
- アサーション
payer
が書き込み可能であり、署名者であること、およびrecipient
が書き込み可能であることを確認しています。これは、転送操作を行うための前提条件です。
- ラムポートの転送
invoke
関数を使用して、payer
からrecipient
へ1,000,000
ラムポートを転送するシステム命令を実行しています。invoke
関数は、実行する命令と、その命令に関連するアカウントのリスト(この場合はpayer
とrecipient
)を引数に取ります。
CPIの重要性
この例では、Solanaプログラムが自身のロジックを実行するだけでなく、他のプログラム(システムプログラムなど)の機能を利用して、より複雑な操作(ここでは資金の転送)を実現しています。
CPIは、Solana上での分散型アプリケーションの構築において重要な役割を果たし、プログラム間の相互作用と協力を可能にします。
PDA のアカウントの作成
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
system_instruction,
system_program,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let vault_pda = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
assert!(payer.is_writable);
assert!(payer.is_signer);
assert!(vault_pda.is_writable);
assert_eq!(vault_pda.owner, &system_program::ID);
assert!(system_program::check_id(system_program.key));
let vault_bump_seed = instruction_data[0];
let vault_seeds = &[b"vault", payer.key.as_ref(), &[vault_bump_seed]];
let expected_vault_pda = Pubkey::create_program_address(vault_seeds, program_id)?;
assert_eq!(vault_pda.key, &expected_vault_pda);
let lamports = 10000000;
let vault_size = 16;
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],
],
]
)?;
Ok(())
}
この関数process_instruction
は、Solanaのスマートコントラクト(プログラム)内で定義され、特定の命令(このケースでは新しいアカウントの作成)を処理するために使用されます。
この関数は、プログラムが他のプログラム(ここではシステムプログラム)をクロスプログラム呼び出し(CPI)で呼び出す際に、プログラム派生アドレス(PDA)を使用して新しいアカウントを作成する方法を示しています。
処理の流れ
ポイント
- アカウント情報の取得
next_account_info
関数を使用して、渡されたアカウントの配列から順番にアカウント情報を取得します。- これには、支払いを行うアカウント(
payer
)、作成されるアカウント(vault_pda
)、そしてシステムプログラムのアカウント(system_program
)が含まれます。
- 条件の検証
- 支払いを行うアカウントが書き込み可能かつ署名者であること、作成されるアカウントが書き込み可能であり、所有者がシステムプログラムであることを確認します。
- システムプログラムのIDが正しいか検証します。
- PDAの検証
instruction_data
の最初のバイトからバンプシードを取得し、Pubkey::create_program_address
を使用して期待されるPDAを生成します。- このPDAが関数に渡された
vault_pda
のキーと一致することを確認します。
- 新しいアカウントの作成
invoke_signed
関数を使って、システムインストラクションのcreate_account
を呼び出し、新しいアカウントを作成します。- この命令には、支払いを行うアカウントのキー、作成されるアカウントのキー(PDA)、転送するラムポートの量、アカウントのサイズ、プログラムのIDが含まれます。
invoke_signed
は、PDAに「署名」するために必要なシード("vault"
、支払いを行うアカウントのキー、バンプシード)も受け取ります。
クロスプログラム呼び出し(CPI)
invoke_signed
を使用して他のプログラムを呼び出す時、プログラムは自らがPDAに対して「署名」するかのように動作できます。
これは、PDAが対応する秘密鍵を持たないため、特定のシードを用いることでプログラムがそのアドレスの所有者として行動できるようにするための仕組みです。
この関数の使用例は、Solana上でプログラムが動的にアカウントを作成し、プログラムのロジックによって管理する場合に見られます。
これにより、アプリケーションやプロトコルは、ユーザーの資産をセキュアに管理したり、特定の条件が満たされたときに自動的に資産を移動させたりすることが可能になります。
クロスプログラム
Solanaブロックチェーンでプログラム間の相互作用を行うためのメカニズム、「クロスプログラム呼び出し(Cross-Program Invocation、CPI)」について説明します。
CPIを利用することで、あるプログラムが別のプログラムの命令を実行できるようになります。
これにより、複数のプログラムが連携してより複雑な操作を実現できるようになります。
CPIの基本的な流れ
- 命令の準備
- あるプログラムが、別のプログラムによって実行される命令(例:トークンの支払い、トークンの送付など)を準備します。
invoke
関数の使用- 呼び出し元のプログラムは、
invoke
またはinvoke_signed
関数を使用して、準備した命令を呼び出します。
この関数はSolanaのランタイムに組み込まれており、指定された命令を該当するプログラムIDにルーティングします。
- 呼び出し元のプログラムは、
- アカウントの検証
- 呼び出し元のプログラムが命令を実行する時、Solanaランタイムは、該当する命令に必要なすべてのアカウントが正しく提供されていることを確認します。
- これは、命令が実行可能なプログラム(executable account)を除くすべての関連アカウントを呼び出し元が提供する必要があるためです。
セキュリティと整合性の確保
- Solanaランタイムは、命令実行前と実行後のアカウントの状態を比較し、プログラムが他のプログラムが所有するアカウントを不正に変更していないことを保証します。
- これにより、プログラム間で安全にデータや資産のやり取りが行えるようになります。
CPIの使用例
- 支払いとアクションの組み合わせ
- あるクライアントが、アリスからのトークン支払いとボブへのミサイル発射という2つのアクションを組み合わせたトランザクションを作成する場合、
acme
プログラムはクライアントに代わってtoken
プログラムのpay()
命令を呼び出すことができます。 - これにより、複数のプログラムが連携して一連の操作を行うことが可能になります。
- あるクライアントが、アリスからのトークン支払いとボブへのミサイル発射という2つのアクションを組み合わせたトランザクションを作成する場合、
CPIは、Solanaプラットフォームにおけるプログラムの柔軟性と機能性を大きく拡張する重要なメカニズムです。
これにより、開発者は異なるプログラムの機能を組み合わせて、より複雑なアプリケーションやサービスを構築できるようになります。
let message = Message::new(vec![
token_instruction::pay(&alice_pubkey),
acme_instruction::launch_missiles(&bob_pubkey),
]);
client.send_and_confirm_message(&[&alice_keypair, &bob_keypair], &message);
このコードは、Solanaブロックチェーン上でクライアントがトランザクションを作成し、そのトランザクションをネットワークに送信するプロセスを示しています。
トランザクションは、特定の命令(instructions
)を含むメッセージ(Message
)で構成され、これらの命令はブロックチェーン上で実行されるアクションを定義します。
具体的に、この例では2つの異なるプログラムへの命令が含まれています。
token_instruction::pay(&alice_pubkey)
- この命令は、トークンプログラム(
token_instruction
)を使用して、アリスの公開鍵(alice_pubkey
)に対して支払いを行います。 - 具体的に何をするか(例えば、どの量のトークンを転送するか)は、
pay
関数の実装に依存しますが、一般的には、アリスにトークンを送る操作を指します。
- この命令は、トークンプログラム(
acme_instruction::launch_missiles(&bob_pubkey)
- こちらは、
acme_instruction
プログラムを使用して、ボブの公開鍵(bob_pubkey
)に関連する何らかのアクション(この例では象徴的に「launch_missiles
」とされています)を行う命令です。 - この命令の具体的な挙動も、
launch_missiles
関数の実装によります。
- こちらは、
これらの命令はMessage::new
関数を通じてメッセージオブジェクトにまとめられ、その後client.send_and_confirm_message
関数によってSolanaネットワークに送信されます。
この関数は、トランザクションをネットワークに送信し、その実行が確認されるまで待機します。
トランザクションに署名するためには、関連するアカウントの秘密鍵が必要であり、この例ではアリスとボブのキーペア(alice_keypair
、bob_keypair
)が使用されています。
このコードは、Solanaプラットフォームで複数のプログラムに対する操作を一つのトランザクションで実行する方法を示しており、ブロックチェーン上で複雑なアクションのシーケンスを効率的に実行する能力を示唆しています。
let message = Message::new(vec![
acme_instruction::pay_and_launch_missiles(&alice_pubkey, &bob_pubkey),
]);
client.send_and_confirm_message(&[&alice_keypair, &bob_keypair], &message);
このコードは、Solanaブロックチェーン上で、特定のプログラム(ここでは架空のacme_instruction
プログラム)が提供する複合的な操作を実行するためのクライアント側の命令を示しています。
具体的には、acme_instruction
プログラム内で定義されたpay_and_launch_missiles
関数を呼び出し、それによってアリスからボブへの支払いと「launch_missiles
」という二つの操作を一つのトランザクション内で実行します。
コードの詳細
- メッセージの作成
Message::new
関数を用いて、実行したい命令(インストラクション)を含む新しいメッセージオブジェクトを作成します。- ここでは、
acme_instruction::pay_and_launch_missiles
関数を呼び出し、その引数としてアリスとボブの公開鍵(alice_pubkey
とbob_pubkey
)を渡しています。 - この命令は、アリスからボブへの支払いと、何らかのアクションを一緒に行うことを意図しています。
- トランザクションの送信と確認
client.send_and_confirm_message
関数によって、作成したメッセージ(トランザクション)をSolanaネットワークに送信し、その処理が完了するまで待機します。- トランザクションには署名が必要であり、この例ではアリスとボブのキーペア(
alice_keypair
、bob_keypair
)が署名者として使用されています。
このコードの意義
このコードは、Solanaプラットフォームのクロスプログラム呼び出し(CPI)を活用して、一つのトランザクション内で複数の異なる操作を連携させる方法を示しています。pay_and_launch_missiles
のような複合的な関数をプログラム内に定義することで、複数のステップを含むビジネスロジックを効率的に実装できます。
これにより、アプリケーションはより高度な機能を、ユーザーに対して透明で簡単な方法で提供することが可能になります。
このアプローチは、特に分散型金融(DeFi)アプリケーションや、複数の異なる条件下で特定のアクションを自動的にトリガーする必要があるシナリオで有用です。
Solanaの高速なトランザクション処理能力と組み合わせることで、複雑な操作も迅速に実行できます。
mod acme {
use token_instruction;
fn launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
...
}
fn pay_and_launch_missiles(accounts: &[AccountInfo]) -> Result<()> {
let alice_pubkey = accounts[1].key;
let instruction = token_instruction::pay(&alice_pubkey);
invoke(&instruction, accounts)?;
launch_missiles(accounts)?;
}
上記のコードは、Solanaブロックチェーンのプログラム開発におけるモジュールと関数の例を示しています。
ここでは、acme
という名前のモジュール内で、token_instruction
を使用してトークンの支払いを行い、その後特定のアクションを実行する2つの関数が定義されています。
コードの概要
- モジュール定義
mod acme
は、acme
という名前のモジュールを定義しています。- このモジュール内で、特定の操作を行うための関数が定義されています。
- 外部モジュールの使用
use token_instruction;
は、token_instruction
モジュール(またはクレート)からの関数や構造体を現在のスコープに導入しています。- これにより、
token_instruction::pay
などの関数を直接呼び出すことができます。
関数の説明
launch_missiles
関数- 特定のアクションを象徴する関数です。
- 実際に何をするかは、
...
(省略された部分)に具体的な実装が含まれることになります。 - この関数は、アカウント情報の配列を受け取り、
Result<()>
型を返します。
pay_and_launch_missiles
関数- この関数は、トークンの支払いと「
launch_missiles
」という2つのアクションを連続して実行します。 - 具体的には、次のステップを含みます。
- アカウント情報の配列からAliceの公開鍵を取得します。
token_instruction::pay
関数を使用して、支払い命令を作成します。この命令は、Aliceに対してトークンを支払うものです。invoke
関数を呼び出して、支払い命令を実行します。この命令が成功した場合、次のステップに進みます。launch_missiles
関数を呼び出して、「launch_missiles
」というアクションを実行します。
- この関数は、トークンの支払いと「
CPI(クロスプログラム呼び出し)の使用
このコード例は、Solanaのクロスプログラム呼び出し(CPI)の概念を示しています。invoke
関数は、他のプログラム(この例ではtoken_instruction
によって定義されたプログラム)の命令を現在のプログラムから実行するために使用されます。
CPIを使用することで、プログラムは他のプログラムの機能を組み合わせて複雑な操作を行うことができます。
権限が必要な命令
Solanaランタイムがプログラム間の呼び出し(クロスプログラム呼び出し、CPI)で特定の権限をどのように扱うかについて説明します。
ここでの「権限」とは、特に署名者(signers
)と書き込み可能(writable
)なアカウントを指します。
権限の拡張
Solanaでは、あるプログラム(呼び出し元)が別のプログラム(呼び出し先)を呼び出す時、呼び出し元プログラムに付与された権限を基に、呼び出し先プログラムにどの権限を拡張できるかをランタイムが決定します。
例えば、呼び出し元プログラムが処理中の命令に署名者や書き込み可能なアカウントが含まれている場合、その署名者やアカウントを含む命令を呼び出し先プログラムに対して発行することができます。
この権限の拡張は、プログラムが不変であるという事実に依存しています。
プログラムのアップグレードという特別なケースを除いて、プログラムのコードは変更されません。
実際の例:acmeプログラム
acmeプログラムの例では、ランタイムはトランザクションの署名を、トークン命令の署名として安全に扱うことができます。
ランタイムがアリスの公開鍵(alice_pubkey
)を参照するトークン命令を見た場合、それがacme命令内で署名済みのアカウントに対応するキーであるかどうかを確認します。
この場合、該当するキーが署名済みのアカウントであれば、ランタイムはトークンプログラムがアリスのアカウントを変更することを認可します。
このプロセスにより、Solanaはプログラム間のセキュアな相互作用を実現し、あるプログラムが持つ権限を別のプログラムに安全に「委譲」することが可能になります。
これによって、複雑なアプリケーションロジックや資産の移動など、様々な操作を効率的かつ安全に行うことができるようになります。
プログラム署名済みアカウント
Solanaプログラムがプログラム派生アドレス(Program Derived Addresses、PDAs)を使用して、元のトランザクションで署名されていないアカウントを含む命令を発行する方法について述べています。
PDAsを使用することで、プログラムは自身が「署名者」として機能し、特定のアカウントへの変更を承認することができます。
プログラム派生アドレス(PDAs)
- PDAsは、特定のプログラムに紐づいているが、対応する秘密鍵を持たない特殊なアドレスです。
- これらは、プログラム自体やその他のパラメータ(シードとしての文字列など)から決定論的に生成されます。
- PDAsを使用することで、プログラムはトランザクションに直接署名することなく、特定の操作を承認することができます。
invoke_signed
関数
invoke_signed
関数は、他のプログラムの命令を呼び出し、その命令に含まれるアカウントをプログラムが「署名」することを可能にします。- これにより、PDAが「署名者」として機能することができます。
- この関数の引数には、実行する命令、命令に関連するアカウントのリスト、そしてPDAを生成するために使用されるシードのリストが含まれます。
使用例
- 上記のコード例では、
invoke_signed
関数を使って、ある命令を発行しています。- この命令では、PDAsを「署名者」として使用しているため、命令に含まれるアカウントに対する操作が承認されます。
- シードのリスト(
&["First addresses seed"]
、&["Second addresses first seed", "Second addresses second seed"]
)は、PDAsの生成に使用されるシードの情報を提供します。- これにより、ランタイムは提供されたアカウントがPDAsによって正しく「署名」されていることを検証できます。
PDAsとinvoke_signed
関数を使用することで、Solanaのプログラムは他のプログラムの命令を安全に呼び出し、特定のアカウントに対する操作を承認することができます。
これにより、プログラムは自身の秘密鍵を公開することなく、トランザクションの一部としてアカウントに署名する能力を持つことができます。
Callの深さ
クロスプログラム呼び出し(Cross-program invocations、CPI)は、Solanaのプログラムが他のプログラムを直接呼び出すことを可能にします。
これにより、プログラム間で機能を共有したり、複合的な操作を実行することができます。
ただし、現在Solanaでは、このような呼び出しの深さ(call depth)には制約があり、最大4レベルまでとなっています。
呼び出しの深さとは
呼び出しの深さとは、あるプログラムから別のプログラムを呼び出し、さらにそのプログラムが他のプログラムを呼び出す、という連鎖が何層にもわたって続くことを指します。
例えば、プログラムAがプログラムBを呼び出し、プログラムBがプログラムCを呼び出す場合、この呼び出しの深さは2となります。
制約の意義
Solanaにおける呼び出しの深さの制約(現在は4レベル)は、システムの安定性とセキュリティを確保するために設けられています。
無制限にプログラム呼び出しができると、無限ループや予期しない複雑な相互作用が発生し、システムのパフォーマンスに悪影響を与えたり、セキュリティ上の脆弱性が生じる可能性があります。
また、計算リソースの消費を適切に管理するためにも、このような制約が必要です。
実践への影響
この制約は、Solanaプログラムを設計する時に考慮すべき重要な点です。
プログラムが他のプログラムを呼び出す際には、この深さの限界を超えないように計画を立てる必要があります。
複雑な操作を実現するために多くのプログラムの連携が必要な場合は、この制約を考慮して設計を行うか、呼び出しの構造を工夫する必要があります。
リリエントランシー
再帰性(Reentrancy)とは、プログラムが実行中に自身を再度呼び出すことを指します。
Solanaにおいて、再帰性は直接的な自己再帰に限定されており、その深さは固定された値で上限が設けられています。
この制限は、プログラムが中間状態から別のプログラムを呼び出した後、予期せずに再度そのプログラム自体が呼び出される状況を防ぐために設けられています。
再帰性の制限の目的
- 状態管理の明確化
- 直接的な自己再帰により、プログラムは自身が再度呼び出された時点での状態を完全にコントロールできます。
これにより、プログラムの状態が予期せずに変更されることを防ぎ、より安定した実行が可能になります。
- 直接的な自己再帰により、プログラムは自身が再度呼び出された時点での状態を完全にコントロールできます。
- セキュリティと安定性の向上
- 中間状態からの再帰的な呼び出しを制限することで、予期しない動作やセキュリティリスク(例えば、再帰による無限ループなど)を最小限に抑えます。
これにより、プログラム全体の安定性と信頼性が向上します。
- 中間状態からの再帰的な呼び出しを制限することで、予期しない動作やセキュリティリスク(例えば、再帰による無限ループなど)を最小限に抑えます。
実践への影響
この制限は、プログラムの設計と開発において重要な考慮事項です。
特に、プログラムが自身の処理の一部として再帰的な動作を必要とする場合、直接的な自己再帰のみが許可されていること、およびその深さに上限があることを意識する必要があります。
開発者は、これらの制約のもとで効率的かつ安全に動作するプログラムを設計するために、再帰の使用を慎重に計画する必要があります。
Solanaのこのような設計は、プログラムの振る舞いをより予測可能にし、複雑な相互作用が生じるリスクを軽減することを目的としています。
これにより、開発者はプログラムの正確な実行フローをより確実に管理できるようになります。
ネイティブプログラム
ネイティブプログラムは、ランタイムと共に配布されるネイティブマシンコードを実行するプログラムで、特定のプログラムIDを持っています。
これらのプログラムは、Solanaの基本的な機能やセキュリティを提供し、他のプログラムから呼び出されることも、トランザクション内の「トップレベル」の命令としてのみ実行されることもあります。
ネイティブプログラムの役割
ポイント
System Program
solana_program::system_program
- 新しいアカウントの作成、アカウントデータの割り当て、アカウントの所有プログラムの指定、System Programが所有するアカウントからのラムポート(Solanaの通貨単位)の転送、トランザクション手数料の支払いなどを行います。
- 他のプログラムから呼び出すことができます。
Compute Budget Program
solana_sdk::compute_budget
- トランザクションに対して追加のCPUまたはメモリリソースを要求します。
- 他のプログラムから呼び出された場合は何もしません。
ed25519 Program
solana_program::ed25519_program
- ed25519署名の検証を行います。
secp256k1 Program
solana_program::secp256k1_program
- secp256k1公開鍵回復操作の検証を行います。
BPF Loader
solana_program::bpf_loader
- 不変のプログラムをブロックチェーン上にデプロイし、実行します。
Upgradable BPF Loader
solana_program::bpf_loader_upgradeable
- アップグレード可能なプログラムをデプロイ、アップグレードし、実行します。
Deprecated BPF Loader
solana_program::bpf_loader_deprecated
- 不変のプログラムをブロックチェーン上にデプロイし、実行します。
使用上の注意
ネイティブプログラムは、その機能や、他のプログラムからの呼び出し可能性に応じて、Solanaプログラム開発者にとって異なる意味を持ちます。
例えば、システムプログラムやBPFローダーは他のプログラムからの命令を受け付けることができ、Solana上でのプログラム開発やアカウント管理に広く利用されます。
一方で、コンピュートバジェットプログラムや署名検証プログラム(ed25519やsecp256k1)は、他のプログラムから直接呼び出すことはできませんが、特定のトランザクションのコンテキスト内でその機能が必要とされる場合があります。
プログラムIDと命令
これらのプログラムは、プログラムIDによって一意に識別され、Solanaのプログラム開発者はこれらのIDを使用して特定のネイティブプログラムの機能にアクセスすることができます。
また、多くのネイティブプログラムには、そのプログラムが処理する命令を表すenum
や、命令を構築するためのコンストラクタが定義されています。
これにより、プログラム間の相互作用が容易になります。
process_instruction関数
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Hello, World!");
Ok(())
}
process_instruction
という関数を定義しています。
引数は以下の3つを受け取ります。
引数
program_id
- プログラムの公開鍵。
accounts
- 関数内で実行される命令でやり取りするアカウント。
instruction_data
- 命令データ。
Hello World!
と出力して処理は終了します。
Entrypoint
entrypoint!(process_instruction);
オンチェーンのプログラムとやり取りするには、プログラムのエントリポイントを定義する必要があります。
エントリーポイントとは?
プログラミング実行時に最初に制御が移る部分のことです。
プログラムが開始されると自動的に特定の場所から実行を開始します。
この部分がエントリーポイントです。
例えば、pythonの場合は if __name__ == "__main__"
という部分がエントリーポイントとなります。
Solanaでは、entrypoint!
という部分からプログラムが開始されるという意味です。
最後に
今回はSolana Playgroundのチュートリアルの1つである「Hello Solana」を進めながら、1つ1つ解説してきました。
第1回の記事ということもあり、だいぶ解説部分が長くなってしまいました。
基礎的な部分を理解する上では役立つと思うので、参考になっていたら嬉しいです。
もし何か質問などがあれば以下のTwitterなどからDMしてください!
普段はPythonやブロックチェーンメインに情報発信をしています。
Twiiterでは図解でわかりやすく解説する投稿をしているのでぜひフォローしてくれると嬉しいです!
参考記事
https://docs.rs/solana-program/latest/solana_program/
https://solana.com/docs/programs
https://solana.com/docs/programs/limitations