Crypto OS
Technical Crypto OS第2段階 · スマートコントラクト開発

T10 · Solana Program

保存領域のないプログラムは、どう状態を記憶する?

この章を 3D で体験する →
練習する能力
Builder
実践
Anchor でカウンタープログラムを書き、Devnet にデプロイしてクライアントから呼び出し、状態を読み戻す。
AI Lab
AI にアカウント検証ロジックを調べさせ、指摘された問題が本当に存在するか自分で確認する。

現実の問題

T7 の保証金コントラクトを Solana へ移そうとして、最初の行で止まります。

balanceOf[ユーザー] += 金額 と書きたいのに、プログラムに保存領域がありません。状態変数も mapping もなく、文書を探しても専用データの保存先が見つかりません。

第二の驚きは、取引を送る時に読み書きする全アカウントを先に列挙することです。プログラムが探すのでなく、呼び出し側がデータを渡します。

第三の驚きも続きます。相手が間違ったアカウントを渡したら、他人のアカウントを渡してそれを変更させたら、どうなるでしょう。

三つは同じ点を示します。データの帰属はチェーンの保証でなく、自分で証明するものです。この章は保存場所と正当性の証明を扱います。

思考実験

ディスクのない純粋関数と考えます。

コードだけで専用保存はありません。呼び出し側からフォルダー群(アカウント)を受け取り、読み、変え、終了します。フォルダーは関数のものではなく、変更を許可されるだけです。

各フォルダーには四情報があります。

アドレス    フォルダーの場所
owner       内容を変更できるプログラム
data        中身のバイト列
lamports    金額。保存用の預託金でもある

owner のプログラムだけが内容を変更できます。 他は読めても変えられません。これが基礎です。

二つを考えます。

第一の問い:違うフォルダーを渡せますか。

できます。自分のカウンターを増やすつもりの命令に他人のものを渡せば、関数にはバイト列しか見えず、そのまま変更します。

期待したフォルダーだと自分で検証する必要があります。 EVM の balanceOf[msg.sender] は自分のマスを指定しますが、ここでは保証を実装します。

第二の問い:専用フォルダーをどう作りますか。

プログラム ID と独自ラベルからアドレスを導きます。誰でも計算できクライアントが指定できますが、誰も秘密鍵を持ちません。導出したプログラムだけが代理で動きます。

balanceOf[ユーザー] はラベルとユーザーから導くアカウントへ変わります。mapping の各マスが、独立して指定できるアカウントです。

  1. 呼び出し側がアカウントを渡す
  2. プログラム自身が検証する
  3. 導出アドレスで帰属を計算し検証する
安全性のほぼすべてが、中間の検証に依存します。

あなたが決める

各ユーザーの increment で増えるカウンターを、自分のものだけ増やせるようにします。

結果を観察する

モデルが安全性と性能を同時に決めます。

方法帰属の保証移転並列性用途
検証なしなし——事故報告
data の authority実行時照合可高い移転可能な資源
導出アドレス導出規則不可高い個別帳簿、設定、金庫
全体の単一アカウント区別しない—低い、書き込みが競合全体設定、統計

習慣にすべき結論です。

EVM が式で与える「自分のマスだけを操作する」保証は、Solana では自分の検証行になります。

優劣でなく取捨選択です。事前宣言で並列性を得る代わりに、検証責任を作者へ移します。

監査の重点も違います。EVM は再入、オーバーフロー、権限、Solana は各種アカウント検証の漏れが中心です。

モデルを作る

五フィールド

Account
 ├─ address      32 バイトのアドレス
 ├─ owner        data の変更権限者。通常ウォレットはシステムプログラム
 ├─ data         任意バイト。プログラムが解釈する
 ├─ lamports     残高。レント免除の閾値も満たす
 └─ executable   プログラムかデータか

三種と役割です。

種類ownerdata変更者
ウォレットシステムプログラム空鍵で署名する人
データ自社プログラム独自構造自社プログラムだけ
プログラムローダー実行コード更新権限者

アカウント検証表

画面に貼るべき一覧です。渡される全アカウントで確認します。

  1. owner は正しいか。 自分のプログラムでなければ信頼できません。
  2. アドレスは正しいか。 派生なら同じラベルで再導出し比較します。
  3. 署名したか。 アドレスだけでなく、本取引の署名が必要です。
  4. 型は正しいか。 同じ長さの別構造を型マーカーで区別します。
  5. 同じものではないか。 別であるべき二アカウントの不一致を確認します。
  6. 書き込み可能か。 変更先を呼び出し側が writable にしていますか。

すべてに実際の攻撃が対応し、署名と owner は特に多く悪用されました。

導出とプログラム間呼び出し

導出アドレスはプログラム ID とラベルから曲線外へ計算し、数学的に対応する秘密鍵がありません。人は鍵で署名できず、プログラムが代理署名します。

二つの用途があります。

  • mapping のマス:[b"counter", ユーザーアドレス] はその人のカウンターです。
  • 金庫:鍵を持つ人でなく、プログラムのロジックだけが出金できます。

プログラム間呼び出しでは導出ラベルで自分の許可を証明します。送金、アカウント作成、他プロトコルとの連携に使います。

検証を宣言にする

六検査の手書きは長く漏れやすいため、Rust のフレームワークはアカウント構造の宣言へ変えます。

use anchor_lang::prelude::*;

// `anchor keys list` の実 ID で置き換える。コピーしたまま使わない
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.authority = ctx.accounts.user.key();
        counter.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        // 本番では checked_add で、オーバーフローを巻き戻りでなくエラーにする
        counter.count = counter.count.checked_add(1).unwrap();
        Ok(())
    }
}

#[account]
pub struct Counter {
    pub authority: Pubkey,   // 32 バイト
    pub count: u64,          // 8 バイト
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // init        本プログラムが作成し owner になる
    // payer       レント免除預託金の支払者
    // space       8 バイトの型マーカー + 32 + 8
    // seeds/bump  ラベルと user から導出。誰でも計算でき、本プログラムだけが書く
    #[account(
        init,
        payer = user,
        space = 8 + 32 + 8,
        seeds = [b"counter", user.key().as_ref()],
        bump
    )]
    pub counter: Account<'info, Counter>,

    #[account(mut)]
    pub user: Signer<'info>,          // Signer が署名を強制

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    // seeds/bump  アドレスを再導出し比較
    // has_one     data の authority と下のアカウントを照合
    #[account(
        mut,
        seeds = [b"counter", authority.key().as_ref()],
        bump,
        has_one = authority
    )]
    pub counter: Account<'info, Counter>,

    pub authority: Signer<'info>,
}

六つのうち五つを委ねます。

  • Account はowner が自分か、型マーカーが一致するか確認します。
  • seeds と bump は再導出と照合です。
  • Signer は署名です。
  • mut は書き込み可能です。
  • has_one はフィールドとアカウントの照合です。

委ねるのは書いた制約だけで、忘れた制約は扱いません。 increment の has_one と seeds を外してもコンパイルと正常テストは合格し、他人のカウンターを変えられます。AI Lab で試します。

何と呼ぶか

Accountアカウント

ウォレット、残高、プログラム、設定という全データの器です。

アドレス、owner、data、lamports が重要です。owner だけが data を変えると覚えれば、混乱が半分減ります。

Programプログラム

Solana のコントラクトで、コードだけの状態なしの存在です。

状態は所有する独立データアカウントにあり、呼び出し側が明示します。

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

ID とラベルから曲線外へ導く、秘密鍵のないアドレスです。

mapping の代わりの保存と金庫に使い、導出プログラムが代理署名できます。

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

送金、作成、連携で他プログラムを呼ぶことです。

ラベルで自分の許可を証明できますが、呼び出す先は外部コードで、信頼しません。

Account Validationアカウント検証

渡されたものが期待したものだと証明します。

owner、導出、署名、型、別物、書き込みの六点です。安全性の中心で、事故の多くはその欠落です。

Rent Exemptionレント免除

サイズ比例の閾値を満たす lamports が保存に必要で、不足なら回収対象です。

保存には預託が必要で、閉鎖時に返還できます。 T11 では支払者の問題になります。

実践

実践Anchor でカウンターを作り、Devnet に配備して状態を読むRust + Solana CLI + Anchor0 円。Devnet 通貨はフォーセットから

複雑さより、開発の全過程と検証の働きを実体験します。

ツールを確認し初期化します。

solana --version
anchor --version
anchor init counter

版は変わるので、固定版の古い教程よりインストール済み版の文書を使います。

プログラムを入力して自分の ID を得ます。

anchor build
anchor keys list      # このプロジェクトのプログラム ID

declare_id! と設定へ反映し再ビルドします。ほぼ誰でも一度忘れ、配備後に ID 不一致になります。

ローカルで試します。 正常な初期化と増加、別ウォレットから元のカウンターの increment を書きます。

第二は拒否される必要があります。 通れば制約が誤りで、正常ケースより重要です。

Devnet に切り替え、取得して配備します。

solana config set --url devnet
solana airdrop 2
anchor deploy

取得には頻度制限があります。実資産を持つウォレットを使いません。

呼んで状態を読みます。 同ラベルでアドレスを計算し、initialize、複数の increment の後に取得して復号し、count を確認します。

クライアントは照会でなく計算で見つけます。 これが工程上の価値です。

CLI でも読みます。

solana account <計算したアカウントアドレス>

owner は自分の ID、data は先頭 8 バイトの型、次の 32 の authority、最後の 8 の count です。手で照合すると理解できます。

壊す実験をします。 Increment の has_one と seeds を外し、再配備して第三の実験を繰り返します。

今度は成功します。 他人の変更を見れば、検証を単なる定型コードとは考えなくなります。

AI Lab

AI LabAI のアカウント検証レビューを、一項目ずつ確かめるLevel 2 · AI Copilot

全コードを渡して三段階で問います。

第一段階:
命令ごと、アカウントごとに、期待するもの、実際の検査、欠けた検査を
表にしてください。修正案は不要です。

第二段階:
最重大な一点の攻撃を、使用アカウント、命令順、結果まで具体化してください。

第三段階:
現在は攻撃に成功し、修正後は拒否されるテストを書いてください。

一覧は整っても、フレームワークが済ませた owner などを欠落と断定する危険があります。誤報で冗長なコードを増やし、本当の漏れを見逃します。

逆に見落としもあります。別のはずが同じアカウント、書くのに writable なしが典型です。

壊れたコードで成功する攻撃テストが、実在を証明します。 前章に続けて繰り返すのは、ここで特に重要だからです。書けなければ主張を疑います。

全制約名も文書検索します。創作する確率は予想より高く、意味が自然なので気づきにくいものです。コンパイル不可は止められても、意味の違う実在名は危険です。

AI の回答後、自分で必ず検証すること

  • 各脆弱性に実際に成功する攻撃取引を作り、証明できないものを疑う
  • 制約名とマクロを公式文書で確認する。もっともらしい創作がある
  • owner や型など、フレームワーク済みの検査を欠陥と誤報しない
  • 同一アカウントと writable の確認を漏らさない
  • 修正がコンパイルでき、攻撃ケースが成功から拒否へ変わる
  • コンパイル、テスト合格、安全性の三つを同一視しない

実例

署名確認がない

管理者アドレスとの一致だけを確認し、署名を調べない、単純でもよく悪用された欠陥です。

誰でも公開アドレスを取引に列挙でき、それだけで通ります。

署名型を使うフレームワークなら防ぎやすくても、手書きでは多発しました。名前の一致は本人の出席ではありません。

型の混同

二用途のデータ長が同じで、型を見ずに期待構造として復号しました。

設定を金庫として渡すと、違う意味で読み書きされます。

8 バイト型マーカーの理由です。手書きでは追加も検証も自分でします。

橋で本物のシステムアカウントを確認しない2022 年、ある大規模ブリッジ

取引内の署名確認命令を調べるため、ランタイム管理の特殊アカウントを読みます。

本物のアドレスか確認せず、攻撃者の偽検証結果を読まされました。

裏付けなしの資産を発行し、億ドル規模の損失になりました。

第二のアドレス検証の欠落です。各行に実際の損失が対応します。

重複アカウント

A から B への移動で、別アドレスか確認しませんでした。

同じものを渡すと読み、減らして書き、また読み増やす処理で第二の書き込みが第一を上書きし、資金を増やす場合があります。

EVM にもありますが、外から渡すため、別物という仮定の根拠がないここでは特に多い問題です。

変数を一つ変える

もしカウンターの owner を確認し忘れる

攻撃者が同レイアウト、自分の authority のアカウントを自分のプログラムで作ります。

構造、authority、署名は正常に見えても、攻撃者が全制御する状態を受け入れます。

内容より先に、データの信頼性を確認します。 第一項の理由です。

もし導出からユーザーを除き固定ラベルだけにする

一人一つから全体一つへ変わります。

帰属がなく誰でも増やせ、全書き込みは待って直列です。

分割粒度が安全境界と並列度を同時に決めます。 EVM の配置以上に後から直しにくい設計です。

もし十数アカウントへ書く命令にする

リストも取引サイズを使い、上限に入らなくなります。

書く先が多いほどロック競合も増えて並列性を失います。

命令分割、ルックアップテーブル、構造再設計が対処です。多くの引数を要するなら、状態分割を見直します。

もしプログラムが金庫から代理送金する

CPI で導出ラベルを渡し、トークンプログラムへ許可を証明します。

その呼び出しへ到達できるコード経路が、金庫の安全性そのものです。一つでも検査不足なら開放されます。

監査では署名付き CPI を全部探し、入口までさかのぼります。

持ち帰る問い

2
なぜブロックチェーンが必要なのか?

対象データの事前宣言で並列実行できる、という工学上の交換です。スループットと作者の検証責任を交換します。チェーン理解はこうした選択の理解です。

9
誰がリスクを負うか?

EVM が防ぐ種類を自分で防ぐため、リスクは作者へ集中します。一覧は推奨でなく納品前の必須項目です。

17
AI が間違えたとき、損失を負うのは誰か?

AI の脆弱性は不存在かもしれず、制約名も創作かもしれません。レビューの価値は結論より、各主張を自分で検証する段階にあります。

章末チェック

要点

PDA はプログラム固有のアドレス空間、CPI はプログラム間の呼び出し方法だ。

この章の実践課題を終えたらチェックしてください進捗をすべて見る

このページの内容