프로그램 파생 주소

솔라나 계정 주소는 블록체인에서 계정의 위치를 가리킵니다. 많은 계정 주소는 keypair의 공개 키이며, 이 경우 해당 개인 키는 계정과 관련된 트랜잭션에 서명하는 데 사용됩니다.

공개 키 주소의 유용한 대안은 프로그램 파생 주소(PDA)입니다. PDA는 프로그램 상태를 저장, 매핑 및 가져오는 쉬운 방법을 제공합니다. PDA는 프로그램 ID와 선택적으로 미리 정의된 입력의 조합을 사용하여 결정론적으로 생성되는 주소입니다. PDA는 공개 키 주소와 유사하게 보이지만 해당하는 개인 키가 없습니다.

솔라나 런타임은 프로그램이 개인 키 없이도 PDA에 서명할 수 있게 합니다. PDA를 사용하면 계정 주소를 추적할 필요가 없습니다. 대신, PDA 파생에 사용된 특정 입력을 기억할 수 있습니다. (프로그램이 서명에 PDA를 사용하는 방법에 대해 알아보려면 크로스 프로그램 호출 섹션을 참조하세요.)

배경

솔라나 keypairEd25519 곡선 (타원 곡선 암호화)의 점입니다. 이는 공개 키와 개인 키로 구성됩니다. 공개 키는 계정 주소가 되고, 개인 키는 계정에 대한 유효한 서명을 생성하는 데 사용됩니다.

곡선 위 주소를 가진 두 계정곡선 위 주소를 가진 두 계정

PDA는 의도적으로 Ed25519 곡선에서 벗어나도록 파생됩니다. 이는 유효한 해당 개인 키가 없으며 암호화 작업(예: 서명 제공)을 수행할 수 없음을 의미합니다. 그러나 솔라나는 프로그램이 개인 키 없이도 PDA에 서명할 수 있게 합니다.

Off Curve AddressOff Curve Address

PDA는 미리 정의된 입력 세트(예: 문자열, 숫자 및 기타 계정 주소)를 사용하여 온체인에 해시맵과 유사한 구조를 만드는 방법으로 생각할 수 있습니다.

Program Derived AddressProgram Derived Address

PDA 도출하기

PDA를 가진 계정을 생성하기 전에 먼저 주소를 도출해야 합니다. PDA를 도출하는 것은 자동으로 해당 주소에 온체인 계정을 생성하지 않습니다 - 계정은 PDA를 도출하는 데 사용된 프로그램을 통해 명시적으로 생성되어야 합니다. PDA는 지도상의 주소와 같다고 생각할 수 있습니다: 주소가 존재한다고 해서 그곳에 무언가가 지어져 있다는 의미는 아닙니다.

Solana SDK는 아래 표에 표시된 함수로 PDA 생성을 지원합니다. 각 함수는 다음 입력을 받습니다:

  • Program ID: PDA를 도출하는 데 사용되는 프로그램의 주소. 이 프로그램은 PDA를 대신하여 서명할 수 있습니다.
  • Optional seeds: 문자열, 숫자 또는 다른 계정 주소와 같은 미리 정의된 입력.
SDK함수
@solana/kit (Typescript)getProgramDerivedAddress
@solana/web3.js (Typescript)findProgramAddressSync
solana_sdk (Rust)find_program_address

이 함수는 program ID와 optional seeds를 사용하여 bump 값을 반복하면서 유효한 프로그램 주소를 생성하려고 시도합니다. bump 값의 반복은 255에서 시작하여 유효한 PDA가 발견될 때까지 1씩 감소합니다. 유효한 PDA가 발견되면 함수는 PDA와 bump seed를 반환합니다.

bump seed는 유효한 off-curve 주소가 생성되도록 optional seeds에 추가되는 추가 바이트입니다.

PDA 도출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);
}
}
Console
Click to execute the code.
bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 250: Error: Invalid seeds, address must fall off the curve
...
// remaining bump outputs

이 예제에서 첫 번째 범프 시드는 오류를 발생시킵니다. 유효한 PDA를 도출하는 첫 번째 범프 시드는 254입니다. 범프 시드 253-251도 고유하고 유효한 PDA를 도출합니다.

이는 동일한 선택적 시드와 programId가 주어졌을 때, 다른 값을 가진 범프 시드도 여전히 유효한 PDA를 도출할 수 있음을 의미합니다.

프로그램에 전달된 PDA가 표준 범프에서 도출되었는지 확인하는 보안 검사를 항상 포함하세요. 그렇지 않으면 프로그램의 명령어에서 예상치 못한 계정이 사용될 수 있는 취약점이 발생할 수 있습니다. PDA를 도출할 때는 표준 범프만 사용하는 것이 가장 좋은 방법입니다.

예제

아래 예제는 Solana SDK를 사용하여 PDA를 도출합니다. ▷ 실행을 클릭하여 코드를 실행하세요.

문자열 시드로 PDA 도출하기

아래 예제는 프로그램 ID와 선택적 문자열 시드를 사용하여 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}`);
Console
Click to execute the code.

주소 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}`);
Console
Click to execute the code.

여러 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}`);
Console
Click to execute the code.

PDA 계정 생성하기

아래 예시는 Anchor 프레임워크를 사용하여 프로그램 파생 주소로 새 계정을 생성합니다. 이 프로그램에는 새 계정을 생성하는 단일 initialize 명령어가 포함되어 있으며, 이 계정은 사용자 주소와 PDA를 도출하는 데 사용된 bump seed를 저장합니다.

Program
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 bumpd
account_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 PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
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에게 PDA를 주소로 사용하여 새 계정을 생성하기 위해 System Program을 호출하도록 지시합니다. PDA를 생성하는 데 사용되는 seeds는 다음과 같습니다:

  • 명령어에 제공된 사용자 계정의 주소
  • 고정 문자열: "data"
  • 표준 bump seed

이 예시에서는 bump 제약 조건에 값이 할당되지 않았으므로 Anchor는 find_program_address를 사용하여 PDA를 도출하고 bump를 찾습니다.

pda_account
#[account(
init,
seeds = [b"data", user.key().as_ref()],
bump,
payer = user,
space = 8 + DataAccount::INIT_SPACE
)]
pub pda_account: Account<'info, DataAccount>,

아래 테스트 파일에는 initialize 명령을 호출하여 프로그램 파생 주소로 새 계정을 생성하는 트랜잭션이 포함되어 있습니다. 이 파일에는 PDA를 도출하는 코드가 포함되어 있습니다.

이 예제는 또한 생성될 새 계정을 가져오는 방법을 보여줍니다.

Test
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 program
const [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 명령을 다시 호출하면 트랜잭션이 실패합니다. 이는 도출된 주소에 이미 계정이 존재하기 때문에 발생합니다.

Is this page helpful?

목차

페이지 편집

관리자

© 2025 솔라나 재단.
모든 권리 보유.