Program Derived Address (PDA)
Program Derived Addresses (PDA) oferują deweloperom na Solanie dwa główne zastosowania:
- Deterministyczne adresy kont: PDA umożliwiają deterministyczne tworzenie adresu przy użyciu kombinacji opcjonalnych "seedów" (zdefiniowanych wcześniej danych wejściowych) oraz konkretnego ID programu.
- Umożliwienie podpisywania przez programy: Środowisko wykonawcze Solany pozwala programom "podpisywać" PDA, które są wyprowadzone z adresu programu.
Można myśleć o PDA jako o sposobie tworzenia struktur podobnych do hashmap na łańcuchu bloków z wcześniej 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 konieczność ś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, który wyprowadził ten adres. Można myśleć o wyprowadzeniu PDA jako o znalezieniu adresu na mapie. Sam adres nie oznacza, że coś zostało zbudowane w tej lokalizacji.
Ta sekcja omawia szczegóły wyprowadzania PDA. Sekcja dotycząca Cross Program Invocations (CPI) wyjaśnia, jak programy używają PDA do podpisywania.
Kluczowe punkty
- PDA to adresy wyprowadzane deterministycznie przy użyciu kombinacji zdefiniowanych wcześniej seedów, bump seeda i ID programu.
- PDA to adresy, które wypadają poza krzywą Ed25519 i nie mają odpowiadającego im klucza prywatnego.
- Programy Solany mogą podpisywać w imieniu PDA 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 ramach programu 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ż zostanie znaleziony ważny 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 wyprowadzić PDA, podaj następujące dane wejściowe do funkcji SDK:
- Wstępnie zdefiniowane opcjonalne "seeds" przekonwertowane na bajty
- ID programu (adres) używany do wyprowadzenia
Gdy zostanie znalezione prawidłowe PDA, funkcja zwraca zarówno adres (PDA), jak i "bump seed" użyty do wyprowadzenia.
Przykłady
Poniższe przykłady pokazują, jak wyprowadzić PDA za pomocą odpowiednich SDK.
Kliknij przycisk "Uruchom", aby wykonać kod.
Wyprowadzenie PDA z opcjonalnym "seed" 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}`);
Wyprowadzenie PDA z opcjonalnym "seed" 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}`);
Wyprowadzenie PDA z wieloma opcjonalnymi "seeds"
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", dodatkowy bajt dołączany do opcjonalnych seedów. 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 seedów (od 255 do 0):
Przykład z Kit nie został 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 seedem, który wyznacza prawidłowy PDA, jest 254.
Zauważ, że bump seedy od 253 do 251 również wyznaczają prawidłowe PDA z różnymi
adresami. Oznacza to, że przy tych samych opcjonalnych seedach i programId
,
bump seed o innej wartości może nadal wyznaczyć prawidłowy PDA.
Tworząc programy Solana, zawsze uwzględniaj kontrole bezpieczeństwa, aby upewnić się, że PDA przekazany do programu jest wyznaczony z kanonicznego bumpa. Brak takich kontroli może wprowadzić luki bezpieczeństwa, które pozwolą na użycie nieoczekiwanych kont w instrukcjach programu. Najlepszą praktyką jest używanie wyłącznie kanonicznego bumpa podczas wyznaczania 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 jedną 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 "seeds" do wyznaczania 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 w Typescript do wyznaczania 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 "seed", transakcja
zakończy się niepowodzeniem. Dzieje się tak, ponieważ konto już istnieje pod
wyznaczonym adresem.
Is this page helpful?