Program Derived Address (PDA)
Program Derived Addresses(PDAs)はSolana上の開発者に2つの主要なユースケースを提供します:
- 決定論的なアカウントアドレス:PDAは、オプションの「seeds」(事前定義された入力)と特定のプログラムIDの組み合わせを使用して、決定論的にアドレスを作成するメカニズムを提供します。
- プログラム署名の有効化:Solanaランタイムは、プログラムがそのプログラムのアドレスから派生したPDAに対して「署名」することを可能にします。
PDAは、事前定義された入力セット(例:文字列、数値、その他のアカウントアドレス)からオンチェーンでハッシュマップのような構造を作成する方法と考えることができます。
このアプローチの利点は、正確なアドレスを追跡する必要がなくなることです。代わりに、その導出に使用された特定の入力を思い出すだけで済みます。
Program Derived Address
Program Derived Address(PDA)を導出するだけでは、そのアドレスにオンチェーンアカウントが自動的に作成されるわけではないことを理解することが重要です。PDAをオンチェーンアドレスとするアカウントは、アドレスの導出に使用されたプログラムを通じて明示的に作成する必要があります。PDAの導出は、地図上でアドレスを見つけるようなものと考えることができます。アドレスがあるだけでは、その場所に何かが構築されているとは限りません。
このセクションではPDAの導出の詳細について説明しています。Cross Program Invocations (CPIs)のセクションでは、プログラムが署名のためにPDAをどのように使用するかを説明しています。
重要なポイント
- PDAは、事前定義されたseeds、bump seed、およびプログラムのIDの組み合わせを使用して決定論的に導出されるアドレスです。
- PDAはEd25519曲線から外れたアドレスであり、対応する秘密鍵がありません。
- SolanaプログラムはそのプログラムIDから導出されたPDAの代わりに署名することができます。
- PDAを導出しても、オンチェーンアカウントが自動的に作成されるわけではありません。
- PDAをアドレスとして使用するアカウントは、Solanaプログラム内の命令を通じて作成する必要があります。
PDAとは
PDAは決定論的に導出されるアドレスで、公開鍵のように見えますが、秘密鍵を持ちません。つまり、そのアドレスに対して有効な署名を生成することはできません。しかし、Solanaランタイムはプログラムが秘密鍵なしでPDAに対して「署名」することを可能にします。
参考として、SolanaのキーペアはEd25519曲線(楕円曲線暗号)上の点であり、公開鍵と対応する秘密鍵を持っています。公開鍵はオンチェーンアカウントのアドレス(一意の識別子)として使用されます。
曲線上のアドレス
PDAは、事前に定義された入力セットを使用して、意図的にEd25519曲線から外れるように導出される点です。Ed25519曲線上にない点は、有効な対応する秘密鍵を持たず、暗号操作(署名)を実行できません。
PDAはオンチェーンアカウントのアドレス(一意の識別子)として機能し、プログラムの状態を簡単に保存、マッピング、取得する方法を提供します。
曲線外のアドレス
PDAの導出方法
PDAの導出には3つの入力が必要です:
- オプションのseed: PDA導出のための事前定義された入力(例:文字列、数値、他のアカウントアドレスなど)。
- bump seed: 有効なPDA(曲線外)が生成されることを保証するためにオプションのseedに追加される追加のバイト。bump seedは255から始まり、有効なPDAが見つかるまで1ずつ減少します。
- プログラムID: PDAが導出されるプログラムのアドレス。このプログラムはPDAの代わりに署名することができます。
PDA導出
PDAを導出するには、それぞれのSDKから以下の関数を使用してください。
SDK | 関数 |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
PDAを導出するには、以下の入力をSDK関数に提供します:
- バイトに変換された事前定義のオプションのseed
- 導出に使用するプログラムID(アドレス)
有効なPDAが見つかると、関数はアドレス(PDA)と導出に使用されたbump seedの両方を返します。
例
以下の例は、それぞれのSDKを使用してPDAを導出する方法を示しています。
「実行」ボタンをクリックしてコードを実行します。
オプションの文字列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を導出する
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を導出する
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}`);
正規バンプ
PDA導出には「バンプシード」が必要です。これは任意のシードに追加される追加バイトです。導出関数は、255から始まり1ずつ減少するバンプ値を反復処理し、有効なオフカーブアドレスが生成されるまで続けます。有効なオフカーブアドレスを生成する最初のバンプ値が「正規バンプ」です。
以下の例では、可能なすべてのバンプシード(255から0)を使用したPDA導出を示しています:
createProgramDerivedAddress 関数がエクスポートされていないため、Kit例は含まれていません。
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はエラーをスローし、有効なPDAを導出する最初のバンプシードは254です。
バンプシード253〜251はすべて異なるアドレスを持つ有効なPDAを導出することに注意してください。これは、同じ任意のシードとprogramId
が与えられた場合でも、異なる値を持つバンプシードでも有効なPDAを導出できることを意味します。
Solanaプログラムを構築する際は、プログラムに渡されるPDAが正規バンプから導出されていることを確認するセキュリティチェックを常に含めてください。これらのチェックを含めないと、予期しないアカウントがプログラム命令で使用されることを許可する脆弱性が生じる可能性があります。PDAを導出する際は正規バンプのみを使用することがベストプラクティスです。
PDAアカウントの作成
以下のサンプルプログラムは、PDAを新しいアカウントのアドレスとして使用してアカウントを作成する方法を示しています。このサンプルプログラムはAnchorフレームワークを使用しています。
このプログラムには、PDAをアカウントのアドレスとして使用して新しいアカウントを作成する単一のinitialize
命令が含まれています。新しいアカウントはuser
のアドレスとPDAの導出に使用されたbump
シードを保存します。
use anchor_lang::prelude::*;declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");#[program]pub mod pda_account {use super::*;pub fn initialize(ctx: Context<Initialize>) -> Result<()> {let account_data = &mut ctx.accounts.pda_account;// store the address of the `user`account_data.user = *ctx.accounts.user.key;// store the canonical bumpaccount_data.bump = ctx.bumps.pda_account;Ok(())}}#[derive(Accounts)]pub struct Initialize<'info> {#[account(mut)]pub user: Signer<'info>,#[account(init,// define the seeds to derive the PDAseeds = [b"data", user.key().as_ref()],// use the canonical bumpbump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,pub system_program: Program<'info, System>,}#[account]#[derive(InitSpace)]pub struct DataAccount {pub user: Pubkey,pub bump: u8,}
この例では、PDA導出のためのseedsには、固定文字列 data
と命令で提供される
user
アカウントのアドレスが含まれています。Anchorフレームワークは自動的に正規の
bump
seedを見つけます。
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
init
制約は、PDAをアドレスとして使用して新しいアカウントを作成するためにSystem
Programを呼び出すようAnchorに指示します。AnchorはこれをCPIを通じて行います。
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
テストファイルにはPDAを導出するためのTypescriptコードが含まれています。
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
テストファイル内のトランザクションは、PDAをアドレスとして使用して新しいオンチェーンアカウントを作成するために
initialize
命令を呼び出します。この例では、Anchorは命令アカウント内のPDAアドレスを推測できるため、明示的に提供する必要はありません。
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});
テストファイルはまた、トランザクションが送信された後にそのアドレスで作成されたオンチェーンアカウントを取得する方法も示しています。
it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});
この例では、同じ user
アドレスをseedとして使用して initialize
命令を複数回呼び出すと、トランザクションは失敗することに注意してください。これは、導出されたアドレスにすでにアカウントが存在するためです。
Is this page helpful?