導入
Solana で何かを実行するとき、実際にネットワークへ送られるのは トランザクション という単位です。ここまで学んできたアカウント・owner の考え方が、実際のやり取りの中でどう組み合わさるのかを見ていきましょう。
説明
1つの トランザクション の中には、1つまたは複数の 命令(instruction) をまとめて入れることができます。それぞれの命令は、次の3つの要素で構成されます。
- どのプログラムを呼び出すか(プログラムID)
- そのプログラムが読み書きする対象のアカウント一覧
- プログラムに渡すデータ(バイト列。関数でいう引数にあたります)
graph TD
TX["トランザクション"] --> I1["命令1<br/>program: TokenProgram<br/>accounts: [送金元, 送金先]<br/>data: 送金額"]
TX --> I2["命令2<br/>program: CounterProgram<br/>accounts: [カウンターアカウント]<br/>data: increment"]
トランザクションには、それを実行してよいという許可を与える 署名者(signer) の署名も必要です。例えば「あなたのウォレットから誰かへ送金する」命令には、あなたの秘密鍵による署名が必須です。署名がなければ、そのアカウントに関わる操作は実行されません。この署名の検証も、前の章で見た Pipelining の中で高速に処理される工程の1つです。
sequenceDiagram
participant Wallet as ユーザーのウォレット(署名者)
participant Cluster as Solanaクラスタ
participant Prog as プログラム
Wallet->>Wallet: トランザクションを組み立てる(命令の配列)
Wallet->>Wallet: 秘密鍵で署名
Wallet->>Cluster: 署名付きトランザクションを送信
Cluster->>Cluster: 署名を検証
Cluster->>Prog: 各命令をプログラムへ渡して実行
Prog-->>Cluster: 対象アカウントのdata/lamportsを更新
クライアント側からトランザクションを組み立てる流れを、簡略化した TypeScript のイメージで見てみましょう。
// クライアント側のイメージ(TypeScript / @solana/web3.js)
const instruction = new TransactionInstruction({
programId: counterProgramId, // 呼び出すプログラム
keys: [
{ pubkey: counterAccount, isSigner: false, isWritable: true },
{ pubkey: wallet.publicKey, isSigner: true, isWritable: false },
],
data: Buffer.from([0]), // "increment" を表すデータ
});
const transaction = new Transaction().add(instruction);
await sendAndConfirmTransaction(connection, transaction, [wallet]);
1つのトランザクションに複数の命令をまとめられることには実用上のメリットがあります。「トークンを送金すると同時に、送金履歴のカウンターも更新する」といった複数の操作を、まとめて成功するか、まとめて失敗するか(アトミック) の単位で実行できるのです。途中の命令だけが実行されて、残りは実行されない、という中途半端な状態にはなりません。
第3章はここまでです。プログラム(コード)とデータアカウントが分かれていること、owner がデータの書き込み権限を握っていること、そしてトランザクションが複数の命令をアトミックにまとめる単位であること ―― この3つが、Solana プログラムを設計するときの基本の型になります。
演習
「送金と同時にログ用のカウンターを更新する」処理を、なぜ2つの別々のトランザクションではなく1つのトランザクションの中の2つの命令としてまとめる方がよいのでしょうか。「アトミック」という言葉を使って説明してみましょう。
ヒント1を見る
別々のトランザクションだと、片方だけ成功して、もう片方が失敗する可能性があります。
ヒント2を見る
同じトランザクション内の複数の命令は、すべて成功するか、すべて失敗するかのどちらかになります。