Program Derived Address (PDA)
솔라나에서 Program Derived Addresses(PDAs)는 개발자에게 두 가지 주요 사용 사례를 제공합니다:
- 결정론적 계정 주소: PDA는 선택적 "seeds"(미리 정의된 입력)와 특정 프로그램 ID의 조합을 사용하여 결정론적으로 주소를 생성하는 메커니즘을 제공합니다.
- 프로그램 서명 활성화: 솔라나 런타임은 프로그램이 자신의 주소에서 파생된 PDA에 대해 "서명"할 수 있도록 합니다.
PDA는 미리 정의된 입력 세트(예: 문자열, 숫자 및 기타 계정 주소)에서 온체인 해시맵과 같은 구조를 만드는 방법이라고 생각할 수 있습니다.
이 접근 방식의 이점은 정확한 주소를 추적할 필요가 없다는 것입니다. 대신, 파생에 사용된 특정 입력만 기억하면 됩니다.
Program Derived Address
Program Derived Address(PDA)를 파생하는 것만으로는 해당 주소에 온체인 계정이 자동으로 생성되지 않는다는 점을 이해하는 것이 중요합니다. PDA를 온체인 주소로 가진 계정은 주소를 파생하는 데 사용된 프로그램을 통해 명시적으로 생성되어야 합니다. PDA를 파생하는 것은 지도에서 주소를 찾는 것과 같다고 생각할 수 있습니다. 주소가 있다고 해서 그 위치에 무언가가 건설되어 있다는 의미는 아닙니다.
이 섹션에서는 PDA 파생에 대한 세부 사항을 다룹니다. Cross Program Invocations (CPIs)에 관한 섹션에서는 프로그램이 서명을 위해 PDA를 사용하는 방법을 설명합니다.
주요 포인트
- PDA는 미리 정의된 seeds, bump seed, 그리고 프로그램 ID의 조합을 사용하여 결정론적으로 파생된 주소입니다.
- PDA는 Ed25519 곡선에서 벗어난 주소이며 해당하는 개인 키가 없습니다.
- 솔라나 프로그램은 자신의 프로그램 ID에서 파생된 PDA를 대신하여 서명할 수 있습니다.
- PDA를 파생하는 것은 자동으로 온체인 계정을 생성하지 않습니다.
- PDA를 주소로 사용하는 계정은 솔라나 프로그램 내의 명령을 통해 생성되어야 합니다.
PDA란 무엇인가
PDA는 결정론적으로 도출되는 주소로, 공개 키처럼 보이지만 개인 키가 없습니다. 이는 해당 주소에 대한 유효한 서명을 생성할 수 없다는 것을 의미합니다. 그러나 Solana 런타임은 프로그램이 개인 키 없이도 PDA를 대신하여 "서명"할 수 있게 합니다.
맥락상, Solana 키페어는 Ed25519 곡선(타원 곡선 암호화)의 점으로, 공개 키와 해당하는 개인 키를 가집니다. 공개 키는 온체인 계정의 주소(고유 식별자)로 사용됩니다.
곡선 위 주소
PDA는 의도적으로 미리 정의된 입력 세트를 사용하여 Ed25519 곡선에서 벗어나도록 도출된 점입니다. Ed25519 곡선 위에 있지 않은 점은 유효한 해당 개인 키가 없으며 암호화 작업(서명)을 수행할 수 없습니다.
PDA는 온체인 계정의 주소(고유 식별자)로 사용될 수 있으며, 프로그램 상태를 쉽게 저장, 매핑 및 가져오는 방법을 제공합니다.
곡선 밖 주소
PDA를 도출하는 방법
PDA 도출에는 세 가지 입력이 필요합니다:
- 선택적 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 파생에는 선택적 시드에 추가되는 추가 바이트인 "bump seed"가 필요합니다. 파생 함수는 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 파생을 위한 seed에는 고정 문자열 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
제약 조건은 Anchor에게 PDA를 주소로 사용하여 새 계정을 생성하기 위해
System Program을 호출하도록 지시합니다. 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?