Program Derived Address (PDA)
Program Derived Addresses (PDAs) oferują deweloperom na Solanie dwa główne zastosowania:
- Deterministyczne adresy kont: PDAs umożliwiają mechanizm deterministycznego tworzenia adresu przy użyciu kombinacji opcjonalnych "seedów" (zdefiniowanych wcześniej danych wejściowych) i konkretnego ID programu.
- Umożliwienie podpisywania przez programy: Środowisko wykonawcze Solany pozwala programom "podpisywać" PDAs, które są wyprowadzone z adresu programu.
Możesz myśleć o PDAs jako o sposobie tworzenia struktur podobnych do hashmap na łańcuchu bloków z zdefiniowanego zestawu danych wejściowych (np. ciągów znaków, liczb i innych adresów kont).
Zaletą tego podejścia jest to, że eliminuje potrzebę śledzenia dokładnego adresu. Zamiast tego wystarczy zapamiętać konkretne dane wejściowe użyte do jego wyprowadzenia.
Program Derived Address
Ważne jest, aby zrozumieć, że samo wyprowadzenie Program Derived Address (PDA) nie tworzy automatycznie konta na łańcuchu bloków pod tym adresem. Konta z PDA jako adresem na łańcuchu muszą być wyraźnie utworzone za pomocą programu użytego do wyprowadzenia adresu. Możesz myśleć o wyprowadzeniu PDA jako o znalezieniu adresu na mapie. Sam adres nie oznacza, że coś zostało zbudowane w tym miejscu.
Ta sekcja obejmuje szczegóły dotyczące wyprowadzania PDAs. Sekcja o Cross Program Invocations (CPIs) wyjaśnia, jak programy używają PDAs do podpisywania.
Kluczowe punkty
- PDAs to adresy wyprowadzane deterministycznie przy użyciu kombinacji zdefiniowanych seedów, bump seeda i ID programu.
- PDAs to adresy, które wypadają poza krzywą Ed25519 i nie mają odpowiadającego im klucza prywatnego.
- Programy Solany mogą podpisywać w imieniu PDAs wyprowadzonych z ich ID programu.
- Wyprowadzenie PDA nie tworzy automatycznie konta na łańcuchu bloków.
- Konto używające PDA jako adresu musi być utworzone za pomocą instrukcji w programie Solany.
Co to jest PDA
PDA to adresy deterministycznie wyprowadzone, które wyglądają jak klucze publiczne, ale nie mają kluczy prywatnych. Oznacza to, że nie jest możliwe wygenerowanie ważnego podpisu dla tego adresu. Jednak środowisko wykonawcze Solana umożliwia programom "podpisywanie" PDA bez potrzeby posiadania klucza prywatnego.
Dla kontekstu, w Solana Keypairs są punktami na krzywej Ed25519 (kryptografia krzywych eliptycznych) z kluczem publicznym i odpowiadającym mu kluczem prywatnym. Klucze publiczne są używane jako adresy (unikalne identyfikatory) dla kont on-chain.
Adres na krzywej
PDA to punkt, który jest celowo wyprowadzony tak, aby znajdował się poza krzywą Ed25519 przy użyciu zdefiniowanego zestawu danych wejściowych. Punkt, który nie znajduje się na krzywej Ed25519, nie ma ważnego odpowiadającego mu klucza prywatnego i nie może wykonywać operacji kryptograficznych (podpisywania).
PDA może służyć jako adres (unikalny identyfikator) dla konta on-chain, zapewniając metodę łatwego przechowywania, mapowania i pobierania stanu programu.
Adres poza krzywą
Jak wyprowadzić PDA
Wyprowadzenie PDA wymaga trzech danych wejściowych:
- Opcjonalne seed-y: Zdefiniowane dane wejściowe (np. ciągi znaków, liczby, inne adresy kont) do wyprowadzenia PDA.
- Bump seed: Dodatkowy bajt dołączony do opcjonalnych seed-ów, aby zapewnić wygenerowanie ważnego PDA (poza krzywą). Bump seed zaczyna się od 255 i zmniejsza się o 1, aż do znalezienia ważnego PDA.
- Program ID: Adres programu, z którego wyprowadzany jest PDA. Ten program może podpisywać się w imieniu PDA.
Wyprowadzenie PDA
Użyj następujących funkcji z odpowiednich SDK, aby wyprowadzić PDA.
SDK | Funkcja |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Aby wyznaczyć PDA, podaj następujące dane wejściowe do funkcji SDK:
- Wstępnie zdefiniowane opcjonalne seed-y przekonwertowane na bajty
- ID programu (adres) używany do wyznaczenia
Gdy zostanie znaleziony prawidłowy PDA, funkcja zwraca zarówno adres (PDA), jak i bump seed użyty do wyznaczenia.
Przykłady
Poniższe przykłady pokazują, jak wyznaczyć PDA za pomocą odpowiednich SDK.
Kliknij przycisk "Uruchom", aby wykonać kod.
Wyznaczanie PDA z opcjonalnym seed-em w postaci ciągu znaków
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}`);
Wyznaczanie PDA z opcjonalnym seed-em w postaci adresu
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}`);
Wyznaczanie PDA z wieloma opcjonalnymi seed-ami
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}`);
Kanoniczny bump
Do wyznaczenia PDA wymagany jest "bump seed", czyli dodatkowy bajt dołączany do opcjonalnych seeds. Funkcja wyznaczania iteruje przez wartości bump, zaczynając od 255 i zmniejszając o 1, aż znajdzie wartość, która generuje prawidłowy adres poza krzywą. Pierwsza wartość bump, która generuje prawidłowy adres poza krzywą, to "kanoniczny bump".
Poniższe przykłady pokazują wyznaczanie PDA przy użyciu wszystkich możliwych bump seeds (od 255 do 0):
Przykład z Kit nie jest uwzględniony, ponieważ funkcja createProgramDerivedAddress nie jest eksportowana.
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 seed 255 generuje błąd, a pierwszym bump seed, który wyznacza prawidłowy PDA, jest 254.
Zauważ, że bump seeds od 253 do 251 również wyznaczają prawidłowe PDA z różnymi
adresami. Oznacza to, że przy tych samych opcjonalnych seeds i programId
, bump
seed o innej wartości nadal może wyznaczyć prawidłowy PDA.
Tworząc programy na Solanie, zawsze uwzględniaj kontrole bezpieczeństwa, aby upewnić się, że PDA przekazany do programu jest wyznaczony z kanonicznego bump. Brak takich kontroli może wprowadzić luki w zabezpieczeniach, które pozwolą na użycie nieoczekiwanych kont w instrukcjach programu. Najlepszą praktyką jest używanie wyłącznie kanonicznego bump przy wyznaczaniu PDA.
Tworzenie kont PDA
Poniższy przykład programu pokazuje, jak utworzyć konto, używając PDA jako adresu nowego konta. Przykład programu korzysta z frameworka Anchor.
Program zawiera pojedynczą instrukcję initialize
do utworzenia nowego konta z
użyciem PDA jako adresu konta. Nowe konto przechowuje adres user
oraz seed
bump
użyty do wyznaczenia PDA.
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,}
W tym przykładzie, seedy do wyznaczenia PDA obejmują stały ciąg znaków data
oraz adres konta user
podany w instrukcji. Framework Anchor automatycznie
znajduje kanoniczny seed bump
.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Ograniczenie init
instruuje Anchor, aby wywołał System Program w celu
utworzenia nowego konta, używając PDA jako adresu. Anchor robi to za pomocą
CPI.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Plik testowy zawiera kod Typescript do wyznaczenia PDA.
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
Transakcja w pliku testowym wywołuje instrukcję initialize
w celu utworzenia
nowego konta on-chain, używając PDA jako adresu. W tym przykładzie Anchor może
wywnioskować adres PDA w kontach instrukcji, więc nie musi być on jawnie podany.
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});
Plik testowy pokazuje również, jak pobrać konto on-chain utworzone pod tym adresem po wysłaniu transakcji.
it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});
Należy zauważyć, że w tym przykładzie, jeśli wywołasz instrukcję initialize
więcej niż raz, używając tego samego adresu user
jako seeda, transakcja
zakończy się niepowodzeniem. Dzieje się tak, ponieważ konto już istnieje pod
wyznaczonym adresem.
Is this page helpful?