プログラム派生アドレス
Solanaのアカウントアドレスは、ブロックチェーン上のアカウントの場所を指します。多くのアカウントアドレスはkeypairの公開鍵であり、その場合、対応する秘密鍵がアカウントに関連するトランザクションの署名に使用されます。
公開鍵アドレスの便利な代替手段として、プログラム派生アドレス(PDA)があります。PDAはプログラムの状態を保存、マッピング、取得するための簡単な方法を提供します。PDAはプログラムIDとオプションの事前定義された入力の組み合わせを使用して決定論的に作成されるアドレスです。PDAは公開鍵アドレスに似ていますが、対応する秘密鍵はありません。
Solanaランタイムは、秘密鍵を必要とせずにプログラムがPDAに署名することを可能にします。PDAを使用することで、アカウントのアドレスを追跡する必要がなくなります。代わりに、PDAの導出に使用された特定の入力を思い出すことができます。(プログラムがPDAを署名にどのように使用するかについては、クロスプログラム呼び出しセクションを参照してください。)
背景
Solanaのkeypairは、Ed25519曲線(楕円曲線暗号)上の点です。これらは公開鍵と秘密鍵で構成されています。公開鍵がアカウントアドレスになり、秘密鍵はアカウントの有効な署名を生成するために使用されます。
曲線上のアドレスを持つ2つのアカウント
PDAは意図的にEd25519曲線から外れるように導出されます。これは、有効な対応する秘密鍵を持たず、暗号操作(署名の提供など)を実行できないことを意味します。ただし、Solanaはプログラムが秘密鍵なしでPDAに署名することを可能にします。
オフカーブアドレス
PDAは、事前に定義された入力セット(例えば、文字列、数値、その他のアカウントアドレスなど)を使用して、オンチェーン上にハッシュマップのような構造を作成する方法と考えることができます。
Program Derived Address
PDAの導出
PDAを持つアカウントを作成する前に、まずアドレスを導出する必要があります。PDAを導出しても、そのアドレスにオンチェーンアカウントが自動的に作成されるわけではありません - アカウントはPDAの導出に使用されたプログラムを通じて明示的に作成する必要があります。PDAは地図上のアドレスのようなものと考えることができます:アドレスが存在するからといって、そこに何かが建てられているわけではありません。
Solana SDKはPDA作成を以下の表に示す関数でサポートしています。各関数は次の入力を受け取ります:
- プログラムID:PDAの導出に使用されるプログラムのアドレス。このプログラムはPDAの代わりに署名することができます。
- オプションのseed:文字列、数値、または他のアカウントアドレスなどの事前定義された入力。
| SDK | 関数 |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
この関数はプログラムIDとオプションのseedを使用し、bump値を反復処理して有効なプログラムアドレスを作成しようとします。bump値の反復は255から始まり、有効なPDAが見つかるまで1ずつ減少します。有効なPDAが見つかると、関数はPDAとbump seedを返します。
bump seedは、有効なオフカーブアドレスが生成されることを保証するために、オプションのseedに追加される追加のバイトです。
PDA導出
正規バンプ
バンプseedは任意のseedsに追加される追加バイトです。導出関数は255から始まり1ずつ減少するバンプ値を反復処理し、有効なオフカーブアドレスが生成されるまで続けます。有効なオフカーブアドレスを生成する最初のバンプ値が「正規バンプ」と呼ばれます。
以下の例では、可能なすべてのバンプseed(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
この例では、最初のバンプseedはエラーをスローします。有効なPDAを導出する最初のバンプseedは254です。バンプseed 253-251も一意の有効なPDAを導出します。
これは、同じ任意のseedsとprogramIdが与えられた場合でも、異なる値を持つバンプseedでも有効なPDAを導出できることを意味します。
プログラムに渡されるPDAが正規バンプから導出されていることを確認するセキュリティチェックを常に含めてください。これを怠ると、プログラムのinstructionsで予期しないアカウントが使用される可能性がある脆弱性が生じる可能性があります。PDAを導出する際は正規バンプのみを使用することがベストプラクティスです。
例
以下の例では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}`);
PDAアカウントを作成する
以下の例では、Anchorフレームワークを使用して、プログラム派生アドレスを持つ新しいアカウントを作成します。このプログラムには、新しいアカウントを作成するための単一のinitialize命令が含まれており、ユーザーアドレスとPDAの導出に使用されたbump seedを保存します。
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 bumpdaccount_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,}
init制約は、AnchorにSystem Programを呼び出すよう指示し、PDAをアドレスとして使用して新しいアカウントを作成します。PDAの作成に使用されるseedsは次のとおりです:
- 命令で提供されるユーザーアカウントのアドレス
- 固定文字列:「data」
- 正規のbump seed
この例では、bump制約に値が割り当てられていないため、Anchorはfind_program_addressを使用してPDAを導出し、bumpを見つけます。
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
以下のテストファイルには、新しいアカウントをプログラム派生アドレスで作成するために
initialize
instructionを呼び出すトランザクションが含まれています。このファイルには
PDAを導出するコードが含まれています。
この例では、作成される新しいアカウントを取得する方法も示しています。
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { PdaAccount } from "../target/types/pda_account";import { PublicKey } from "@solana/web3.js";describe("pda-account", () => {const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.PdaAccount as Program<PdaAccount>;const user = provider.wallet as anchor.Wallet;// Derive the PDA address using the seeds specified on the programconst [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);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
instructionを再度呼び出すと、トランザクションは失敗します。これは、導出されたアドレスにすでにアカウントが存在するためです。
Is this page helpful?