요약
PDA는 시드 + 프로그램 ID + 범프를 SHA-256으로 해싱하여 결과가 Ed25519 곡선 밖에 있을 때까지 유도됩니다. 정규 범프는 곡선 밖 주소를 생성하는 첫 번째 값입니다. 최대 16개의 시드, 시드당 최대 32바이트.
배경
Solana
Keypair
값은 Ed25519 곡선의 점입니다. 키페어는 공개 키(계정
주소로 사용)와 비밀 키(서명 생성에 사용)로 구성됩니다. 비밀 키를 가진 사람은
누구나 해당 주소에 대한 트랜잭션에 서명할 수 있습니다.
곡선 위 주소를 가진 두 개의 계정
PDA는 의도적으로 Ed25519 곡선 밖에 위치하도록 유도됩니다. 유효한 곡선 점이
아니기 때문에 비밀 키가 존재하지 않으며, 외부 당사자는 서명을 생성할 수
없습니다. 유도 프로그램만이 *invoke_signed*를 통해 PDA에 대한 작업을 승인할 수
있습니다.
곡선 밖 주소
PDA vs 키페어 계정
| 속성 | 키페어 계정 | PDA 계정 |
|---|---|---|
| 주소 유형 | Ed25519 곡선 위 | Ed25519 곡선 밖 |
| 개인 키 보유 | 예 | 아니오 |
| 트랜잭션 서명 가능 | 예(개인 키 사용) | 아니오 |
| CPI 중 서명 가능 | 아니오(트랜잭션에 서명이 포함되지 않은 경우) | 예(invoke_signed를 통해) |
| 유도 | Ed25519 키페어 생성 | 시드 + 프로그램 ID로부터 결정적 |
| 일반적인 용도 | 사용자 지갑, 프로그램 ID | 프로그램 소유 데이터 계정 |
선택적 시드
선택적 시드는 PDA 파생의 입력으로 사용되는 사용자 정의 바이트 문자열입니다.
이들은 프로그램에 범위가 지정된 고유하고 결정론적인 주소를 생성합니다. 예를
들어, ["user", user_pubkey]를 시드로 사용하면 각 사용자에 대해 서로 다른 PDA가
파생됩니다.
시드는 다음 제약 조건을 따라야 합니다:
- 파생당 최대 16개의 시드 (
MAX_SEEDS) - 시드당 최대 32바이트 (
MAX_SEED_LEN)
범프 시드
범프 시드는 파생 중에 선택적 시드에 추가되는 단일 바이트(0-255)입니다.
find_program_address는
255부터 0까지 검색하며, 결과가 Ed25519 곡선에서 벗어날 때까지 각 값으로
*rscreate_program_address*를 호출합니다. 성공하는 첫 번째 값이 정규
범프입니다.
프로그램은 시드에서 주소로의 고유하고 결정론적인 매핑을 보장하기 위해 항상 정규 범프를 사용해야 합니다.
PDA를 파생할 때는 항상 정규 범프를 사용하세요. 비정규 범프를 사용하면 동일한 시드에 대해 두 번째 유효한 주소가 생성되어 공격자가 예상과 다른 계정을 대체할 수 있는 취약점이 발생할 수 있습니다.
PDA 파생
파생 알고리즘
PDA 파생은 SDK의
create_program_address
함수에 구현되어 있습니다. 알고리즘은 다음과 같이 작동합니다:
- 시드의 수가
MAX_SEEDS(16)를 초과하지 않고 개별 시드가MAX_SEED_LEN(32바이트)를 초과하지 않는지 검증합니다. 두 검사 중 하나라도 실패하면 *rsPubkeyError::MaxSeedLengthExceeded*를 반환합니다. - 모든 시드, 프로그램 ID, 그리고 문자열
"ProgramDerivedAddress"를 함께 SHA-256 해시하여 32바이트 결과를 생성합니다. - 결과가 Ed25519 곡선의 유효한 점인지 확인합니다.
- 결과가 곡선 위에 있으면 *rs
PubkeyError::InvalidSeeds*를 반환합니다(주소에 해당하는 개인 키가 있어 PDA 보안 속성을 위반함). - 결과가 곡선 위에 없으면 이를 PDA로 반환합니다.
컴퓨트 유닛 비용
*rscreate_program_address*를 위한
온체인 syscall은
호출당
1,500 CU를
청구합니다.
try_find_program_address syscall은
진입 시(루프 이전) 1,500 CU를 청구하고, 루프 내에서 실패한 각 범프 시도마다
추가로 1,500 CU를 청구합니다.
일반적인 seed 패턴
Seed는 애플리케이션별로 다릅니다. 일반적인 패턴은 다음과 같습니다:
| 패턴 | Seed | 사용 사례 |
|---|---|---|
| 전역 싱글톤 | ["global"] | 프로그램 전체 단일 설정 계정 |
| 사용자별 계정 | ["user", user_pubkey] | 프로그램당 사용자당 하나의 계정 |
| 사용자별-엔티티별 | ["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를 대신하여 서명할 수 있습니다.
- 선택적 seed: 문자열, 숫자 또는 기타 계정 주소와 같은 미리 정의된 입력입니다.
| 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*가 정규 범프를 반환하는 방법을
설명합니다:
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
이 예제에서 bump 255는 곡선 위의 주소를 생성하여 실패합니다. 첫 번째 유효한 bump는 254이며, 이것이 정규 bump가 됩니다.
Is this page helpful?