導入
プログラムがちゃんと動いているか確認したいとき、普通のアプリなら print やデバッガを使いますよね。Solana プログラムにも同じような手段があります。そして「成功」と「失敗」をどう表現するかも、Solana 独特のお作法があります。
説明
Solana のプログラムの中からログを出すには msg! マクロを使います。これは Rust 標準の println! に似ていますが、出力先は自分のパソコンの画面ではなく、トランザクションのログ(solana logs コマンドや、ブロックエクスプローラで見られる実行記録)です。
use solana_program::msg;
msg!("counter received: {}", 42);
msg!("処理を開始します");
そして process_instruction の戻り値の型は ProgramResult でした。これは Rust の Result<(), ProgramError> の別名(type alias)で、「成功したら空の値、失敗したらエラー情報を返す」という Rust の Result の考え方そのものです。
use solana_program::{entrypoint::ProgramResult, program_error::ProgramError};
fn process_instruction(/* ... */) -> ProgramResult {
let ok = true;
if !ok {
// 失敗: エラーの種類を添えて返す。ここでトランザクション全体が失敗(ロールバック)する
return Err(ProgramError::InvalidArgument);
}
msg!("成功しました");
// 成功: 空のOkを返す
Ok(())
}
flowchart TD
P[process_instructionの処理] --> C{条件を満たす?}
C -- はい --> OK["Ok(())を返す<br/>→ トランザクション成功・状態変更が確定"]
C -- いいえ --> ERR["Err(ProgramError::...)を返す<br/>→ トランザクション全体が失敗・全ての変更が取り消される"]
ここで重要なのは、1つでも Err を返すと、その命令だけでなくトランザクション全体の状態変更が取り消されるという点です。途中まで実行されて中途半端な状態が残る、ということは起きません。これは「原子性(atomicity)」と呼ばれる性質で、金銭を扱うシステムにとって欠かせない安全設計です。
ProgramError にはあらかじめ InvalidArgument・InvalidAccountData・MissingRequiredSignature など、よく使うエラーの種類が用意されています。独自のエラー種別を作りたい場合は、自分の enum を定義して ProgramError::Custom(u32) に変換する、という拡張方法もありますが、まずは組み込みのエラーで十分表現できることが多いです。
演習
Err を返すとトランザクション全体が取り消される、という性質が「銀行の送金」のようなお金を扱う処理にとってなぜ重要か、具体例を考えて説明してみましょう(例: 送金元から引き落とすところまで成功して、送金先への入金だけ失敗したら?)。
ヒント1を見る
「引き落としは成功したのに入金は失敗した」という状態が残ると何が起きるか考えてみましょう。
ヒント2を見る
原子性(all or nothing)があるおかげで、そうした中途半端な状態が絶対に残りません。