Solana에서 빌드하며 일주일 동안 PDA에 대해 배운 것들 - Hala Kabir
요약
Solana의 stateless 아키텍처에서 데이터를 저장하기 위한 핵심 메커니즘인 PDA(Program Derived Addresses)의 개념과 작동 원리를 설명합니다. PDA를 데이터베이스의 복합 키와 비교하며, 결정론적 유도 방식과 프로그램 독점 권한의 중요성을 다룹니다.
핵심 포인트
- Solana 프로그램은 상태를 저장할 수 없어 별도의 계정이 필요함
- PDA는 수학적으로 유도되는 결정론적 데이터베이스 기본 키와 유사함
- 프로그램 ID를 포함하여 특정 프로그램만이 해당 주소를 제어함
- Bump를 사용하여 Ed25519 곡선 밖의 주소를 생성함으로써 보안 유지
Solana에서 스마트 컨트랙트 (programs)는 완전히 상태가 없는 (stateless) 로직 엔진입니다. 서버가 컨텍스트를 보유하거나 자체 메모리를 수정하는 Web2 백엔드와 달리, Solana 프로그램은 내부적으로 단 1바이트의 데이터도 저장할 수 없습니다. 만약 프로그램이 사용자의 트랜잭션 횟수, 게임 상태, 또는 글로벌 설정과 같은 실행 상태를 기억해야 한다면, 이는 반드시 완전히 분리된 계정 (account)에 존재해야 합니다. 프로그램 유도 주소 (Program Derived Addresses, PDAs)는 이러한 아키텍처 요구 사항에 대한 궁극적인 해결책이며, 중앙 집중식 레지스트리를 유지하지 않고도 온체인 (on-chain) 및 오프체인 (off-chain)에서 데이터를 찾고 검증할 수 있는 결정론적 (deterministic) 메커니즘을 제공합니다.
멘탈 모델 (The Mental Model)
만약 당신이 Web2 또는 전통적인 관계형 데이터베이스 (relational database) 배경을 가지고 있다면, PDA를 생각하는 가장 깔끔한 방법은 행 (row)의 논리적 식별자로부터 직접 계산되는 결정론적 데이터베이스 기본 키 (primary key)로 간주하는 것입니다.
SQL 데이터베이스에서는 WHERE user_id = X AND table_name = 'profiles'와 같은 복합 키 (compound key)를 사용하여 사용자의 프로필을 찾을 수 있습니다. PDA는 정확히 이 복합 키처럼 작동하며, 32바이트 공개 키 (public key) 문자열로 해싱됩니다. 하지만 이 비유는 두 가지 결정적인 지점에서 깨집니다:
-
온디맨드 생성 (On-Demand Generation): PDA는 원장 (ledger)의 어디에도 마스터 인덱스 테이블에 저장되어 있지 않습니다. 이들은 수학적으로 즉석에서 유도됩니다. 특정 주소는 온체인 상에서 실제 계정이 해당 위치에 초기화되기 훨씬 전부터 암호학적으로 존재할 수 있습니다.
-
프로그램 접근 독점 (Program Access Monopolies): 유도 레이아웃 (derivation layout)은 암호학적 해시 함수 (cryptographic hash function)에 당신의 특정 프로그램 ID (Program ID)를 명시적으로 포함합니다. 이는 오직 당신의 프로그램만이 해당 정확한 주소를 생성할 수 있고, 해당 주소를 대신하여 트랜잭션에 서명할 수 있음을 보장합니다.
유도 구조 (Anatomy of a Derivation)
표준 Anchor 카운터 구현의 정석적인 패턴을 살펴보겠습니다:
Rust
#[account(
init,
...
이 유도 과정 동안 Anchor가 배후에서 정확히 무엇을 하고 있는지 분석해 보겠습니다:
- 정적 시드 접두사 (Static Seed Prefix,
b"counter"): 이는 우리의 "테이블 이름" 제약 조건 역할을 합니다. 이를 통해 이 PDA 공간이 동일한 프로그램 내의 다른 데이터 스키마와 충돌하지 않도록 보장합니다. - 동적 시드 (Dynamic Seed,
user.key().as_ref()): 이는 고유 식별 구성 요소(사용자 ID와 같은 역할)입니다. 결과 주소를 호출자의 공개 키(Public Key)에 직접 결합합니다. - 프로그램 ID (The Program ID): 배열에 명시적으로 나열되지는 않지만, Anchor는 유도(Derivation) 계산 과정에 여러분의 프로그램 컴파일 ID를 암묵적으로 주입합니다.
- 범프 (The Bump): 이것이 핵심적인 부분입니다. Solana 주소가 표준 키페어(Keypair) 지갑에 속하려면 Ed25519 곡선(Ed25519 curve)이라고 불리는 대수적 경로 위에 있어야 합니다. PDA는 정의상 개인 키(Private Key)를 가져서는 안 됩니다(그렇지 않으면 누군가 이를 추측하여 여러분의 데이터에 서명할 수 있기 때문입니다).
bump는 런타임이 해시를 Ed25519 곡선에서 완전히 벗어나도록 자동으로 계산하는 단일 바이트 카운터(255에서 시작하여 감소함)입니다.
시드가 중요한 이유
시드 배열의 구조는 애플리케이션 전체의 보안과 아키텍처를 결정합니다. 다음 두 가지 시드 구성의 차이점을 살펴보세요:
seeds = [b"counter", user.key().as_ref()]seeds = [b"counter"]
첫 번째 구성은 사용자당 격리된 상태를 생성합니다. 지갑 A와 지갑 B가 유도를 실행하면 완전히 고유한 주소를 얻게 됩니다. 이는 보안이 유지되는 사용자별 추적 상태입니다.
두 번째 구성은 개별 식별성을 완전히 제거합니다. 누가 유도를 호출하든 결과는 정확히 동일한 공개 키가 됩니다. 이는 전역 프로그램 규칙을 규제하는 **글로벌 설정 싱글톤 (Global Config Singleton)**에는 완벽하지만, 사용자 상태(User State)로 사용하기에는 재앙이 될 것입니다. 초기화하는 첫 번째 사람이 "계정이 이미 사용 중임 (Account Already In Use)" 오류로 다른 모든 사람을 차단하게 될 것이기 때문입니다!
범프(Bump)를 통해 얻는 것
Anchor의 네이티브 bump 제약 조건을 사용하면, 시스템은 주소를 암호화 곡선에서 성공적으로 밀어내는 가장 높은 시작 바이트 값(255)인 캐노니컬 범프(Canonical Bump)를 계산합니다.
Anchor는 초기화(initialization) 과정에서 계산된 이 범프(bump)를 계정 구조(account structure)에 자동으로 저장합니다. 이후의 명령(instruction)에서는 새로운 범프를 계산하는 대신, 항상 이 저장된 범프 값을 읽어서 다시 전달해야 합니다. 실시간으로 범프를 재유도(re-deriving)하는 것은 CPU가 검색 루프(search loop)를 실행하도록 강제하여 컴퓨팅 예산(compute budgets)을 낭비하게 하는 반면, 계정 구조에서 저장된 범프를 읽어오는 것은 사실상 비용이 들지 않습니다.
전체 라이프사이클 (The Full Lifecycle)
Solana PDA의 라이프사이클은 네 가지 명확한 단계를 거칩니다:
- 유도 (Derive): 클라이언트 또는 프로그램이 시드(seeds)를 사용하여 수학적으로 주소를 계산합니다.
- 초기화 (Initialize,
init): 프로그램이 시스템 프로그램(System Program)을 호출하고, 필요한 렌트(rent, 계정을 온체인에 유지하기 위해 예치되는 소량의 lamports)를 전달하며, 필요한 공간을 할당하고 계정을 생성합니다. - 변형 (Mutate): 명령(instructions)이 해당 계정을 수락하고, 시드가 일치하는지 검증하며, 내부 데이터 속성을 변경합니다 (예: 카운터 증가).
- 종료 (Close): 데이터가 더 이상 필요하지 않을 때, 명령을 통해 데이터 배열을 0으로 만들고 남은 모든 lamports를 사용자에게 다시 전송할 수 있습니다. Solana에서 "종료"는 단순히 즉각적인 렌트 소진(rent drainage)을 의미합니다. 계정의 lamports가 0에 도달하면, 검증인(validators)은 슬롯(slot) 끝에 해당 계정을 가비지 컬렉션(garbage collection) 대상으로 즉시 표시합니다.
과거의 나에게 해주고 싶은 말
- 프로그램 ID는 하드코딩된 닻(anchors)입니다: 다른 프로그램 ID에서 실행되는 정확히 동일한 시드 배열은 완전히 다른 주소를 생성하게 됩니다.
- 키가 아니라 프로그램이 서명합니다: PDA는 개인 키(private keys)를 소유하지 않습니다. PDA는 귀하의 특정 프로그램이
invoke_signed를 사용하여 대신 서명할 때만 동작을 실행할 수 있습니다. init_if_needed를 주의하세요: 매우 편리하지만, 엄격한 제약 조건을 적용하지 않으면 누락된 로직 경계를 쉽게 숨기거나 재진입(reentrancy) 취약점을 유발할 수 있습니다. 지름길이 아닌, 의도적인 선택으로서 사용하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기