概要
PDA は、seed + プログラム ID + bump を SHA-256 でハッシュ化し、結果が Ed25519 曲線外になるまで導出されます。正規 bump は、曲線外アドレスを生成する最初の値です。最大 16 個の seed、seed あたり最大 32 バイト。
背景
Solana の
Keypair
値は、Ed25519 曲線上の点です。keypair は、公開鍵(アカウントアドレスとして使用)と秘密鍵(署名の生成に使用)で構成されます。秘密鍵を持つ者は誰でも、そのアドレスのトランザクションに署名できます。
曲線上アドレスを持つ2つのアカウント
PDA は意図的に Ed25519 曲線外に導出されます。有効な曲線上の点ではないため、秘密鍵は存在せず、外部の当事者は署名を生成できません。導出プログラムのみが
invoke_signed を通じて PDA の操作を承認できます。
曲線外アドレス
PDA と keypair アカウント
| プロパティ | keypair アカウント | PDA アカウント |
|---|---|---|
| アドレスタイプ | Ed25519 曲線上 | Ed25519 曲線外 |
| 秘密鍵の有無 | あり | なし |
| トランザクションへの署名 | 可能(秘密鍵使用) | 不可 |
| CPI 中の署名 | 不可(トランザクションに署名が含まれる場合を除く) | 可能(invoke_signed 経由) |
| 導出 | Ed25519 keypair を生成 | seed + プログラム ID から決定論的 |
| 一般的な用途 | ユーザーウォレット、プログラム ID | プログラム所有のデータアカウント |
オプションのシード
オプションのシードは、PDA導出の入力として機能するユーザー定義のバイト文字列です。これらは、プログラムにスコープされた一意で決定論的なアドレスを作成します。たとえば、["user", user_pubkey]をシードとして使用すると、ユーザーごとに異なるPDAが導出されます。
シードは以下の制約に従う必要があります:
- 1回の導出あたり最大16個のシード (
MAX_SEEDS) - 1つのシードあたり最大32バイト (
MAX_SEED_LEN)
バンプシード
バンプシードは、導出時にオプションのシードに追加される1バイト(0-255)です。
find_program_address
は255から0まで検索し、各値で create_program_address
を呼び出し、結果がEd25519曲線から外れるまで続けます。最初に成功した値が正規バンプです。
プログラムは、シードからアドレスへの一意で決定論的なマッピングを保証するために、常に正規バンプを使用する必要があります。
PDAを導出する際は、常に正規バンプを使用してください。非正規バンプを使用すると、同じシードに対して2つ目の有効なアドレスが作成され、攻撃者が予期しない別のアカウントに置き換える脆弱性につながる可能性があります。
PDA導出
導出アルゴリズム
PDA導出は、SDKの
create_program_address
関数で実装されています。アルゴリズムは以下のように動作します:
- シードの数が
MAX_SEEDS(16)を超えず、個々のシードがMAX_SEED_LEN(32バイト)を超えないことを検証します。いずれかのチェックが失敗した場合、PubkeyError::MaxSeedLengthExceededを返します。 - すべてのシード、プログラムID、および文字列
"ProgramDerivedAddress"を一緒にSHA-256ハッシュ化して、32バイトの結果を生成します。 - 結果がEd25519曲線上の有効な点であるかどうかを確認します。
- 結果が曲線上にある場合、
PubkeyError::InvalidSeedsを返します(アドレスに対応する秘密鍵が存在することになり、PDAのセキュリティ特性に違反します)。 - 結果が曲線上にない場合、それをPDAとして返します。
コンピュートユニットコスト
*rscreate_program_address*のオンチェーンsyscallは、呼び出しごとに1,500 CUを消費します。
try_find_program_address syscallは、エントリ時(ループ前)に1,500
CUを消費し、ループ内でバンプ試行が失敗するたびに追加で1,500 CUを消費します。
一般的なseedパターン
Seedはアプリケーション固有です。一般的なパターンには以下が含まれます:
| パターン | Seeds | ユースケース |
|---|---|---|
| グローバルシングルトン | ["global"] | プログラム全体で単一の設定アカウント |
| ユーザーごとのアカウント | ["user", user_pubkey] | プログラムごとにユーザーごとに1つのアカウント |
| ユーザーごと・エンティティごと | ["vault", user_pubkey, mint_pubkey] | トークンボールト、ユーザーごと・トークンごと |
| カウンター/連番 | ["order", user_pubkey, &order_id.to_le_bytes()] | ユーザーごとの連続レコード |
Seedはハッシュ化前に連結されるため、["ab", "cd"]と["abcd"]は同じPDAを生成します。衝突を避けるため、固定長のseedまたはセパレータを使用してください。例えば、["ab", "-", "cd"]は明確です。
例: PDAの導出
PDAの導出はアドレスのみを計算します。そのアドレスにオンチェーンアカウントを作成するわけではありません。アカウントは別の命令(通常はCPI経由のcreate_account)を通じて明示的に作成する必要があります。
Solana SDKはPDA導出用の関数を提供しています。各関数は以下を受け取ります:
- プログラムID: PDAの導出に使用されるプログラムのアドレス。このプログラムはPDAの代わりに署名できます。
- オプションのseeds: 文字列、数値、または他のアカウントアドレスなどの事前定義された入力。
| SDK | 関数 |
|---|---|
@solana/kit (TypeScript) | getProgramDerivedAddress |
@solana/web3.js (TypeScript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
以下の例では、Solana SDKを使用してPDAを導出します。▷ 実行をクリックしてコードを実行してください。
文字列seedを使用したPDAの導出
以下の例では、プログラムIDとオプションの文字列seedを使用してPDAを導出します。
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
アドレスseedを使用したPDAの導出
以下の例では、プログラムIDとオプションのアドレスseedを使用してPDAを導出します。
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
複数のseedを使用したPDAの導出
以下の例では、プログラムIDと複数のオプションseedを使用してPDAを導出します。
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const optionalSeedString = "helloWorld";const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedString, optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
すべてのバンプの反復処理
以下の例では、すべての可能なバンプseed(255から0)を使用したPDA導出を示しており、find_program_addressが正規バンプを返す仕組みを説明しています。
Kitの例は含まれていません。これは、createProgramDerivedAddress関数がエクスポートされていないためです。
import { PublicKey } from "@solana/web3.js";const programId = new PublicKey("11111111111111111111111111111111");const optionalSeed = "helloWorld";// Loop through all bump seeds (255 down to 0)for (let bump = 255; bump >= 0; bump--) {try {const PDA = PublicKey.createProgramAddressSync([Buffer.from(optionalSeed), Buffer.from([bump])],programId);console.log("bump " + bump + ": " + PDA);} catch (error) {console.log("bump " + bump + ": " + error);}}
bump 255: Error: Invalid seeds, address must fall off the curvebump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6Xbump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4ybump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHHbump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdPbump 250: Error: Invalid seeds, address must fall off the curve...// remaining bump outputs
この例では、バンプ255は曲線上のアドレスを生成するため失敗します。最初の有効なバンプは254であり、これが正規のバンプとなります。
Is this page helpful?