Podsumowanie
PDA są wyprowadzane przez haszowanie seedów + program ID + bump za pomocą SHA-256, aż wynik znajdzie się poza krzywą Ed25519. Kanoniczny bump to pierwsza wartość, która daje adres poza krzywą. Maksymalnie 16 seedów, maksymalnie 32 bajty na seed.
Tło
Wartości Solana
Keypair
są punktami na krzywej Ed25519. Keypair składa się
z klucza publicznego (używanego jako adres konta) oraz klucza prywatnego
(służącego do generowania podpisów). Każdy, kto posiada klucz prywatny, może
podpisywać transakcje dla tego adresu.
Dwa konta z adresami na krzywej
PDA jest celowo wyprowadzane tak, aby znajdowało się poza krzywą Ed25519.
Ponieważ nie jest to prawidłowy punkt na krzywej, nie istnieje żaden klucz
prywatny i żadna zewnętrzna strona nie może wygenerować podpisu. Tylko program
wyprowadzający może autoryzować operacje na PDA przez invoke_signed.
Adres poza krzywą
PDA kontra konta keypair
| Właściwość | Konto keypair | Konto PDA |
|---|---|---|
| Typ adresu | Na krzywej Ed25519 | Poza krzywą Ed25519 |
| Ma klucz prywatny | Tak | Nie |
| Może podpisywać transakcje | Tak (za pomocą klucza prywatnego) | Nie |
| Może podpisywać podczas CPI | Nie (chyba że podpis dołączony do transakcji) | Tak (przez invoke_signed) |
| Wyprowadzanie | Generowanie keypair Ed25519 | Deterministycznie z seedów + program ID |
| Typowe zastosowanie | Portfele użytkowników, Program ID | Konta danych należące do programu |
Opcjonalne seedy
Opcjonalne seedy to zdefiniowane przez użytkownika ciągi bajtów, które służą
jako dane wejściowe do wyznaczania PDA. Pozwalają one tworzyć unikalne,
deterministyczne adresy przypisane do konkretnego programu. Na przykład użycie
["user", user_pubkey] jako seedów powoduje wyznaczenie innego PDA dla każdego
użytkownika.
Seed musi spełniać następujące ograniczenia:
- Maksymalnie 16 seedów na wyznaczenie (
MAX_SEEDS) - Maksymalnie 32 bajty na jeden seed (
MAX_SEED_LEN)
Bump seed
Bump seed to pojedynczy bajt (0-255) dołączany do opcjonalnych seedów podczas
wyznaczania.
find_program_address
przeszukuje wartości od 255 do 0, wywołując create_program_address z każdą
z nich, dopóki wynik nie wypadnie poza krzywą Ed25519. Pierwsza wartość, która
się powiedzie, to kanoniczny bump.
Programy powinny zawsze używać kanonicznego bumpa, aby zapewnić unikalne i deterministyczne mapowanie seedów na adres.
Zawsze używaj kanonicznego bumpa podczas wyznaczania PDA. Użycie niekanonicznego bumpa tworzy drugi poprawny adres dla tych samych seedów, co może prowadzić do podatności, w których atakujący podmieni konto na inne niż oczekiwane.
Wyznaczanie PDA
Algorytm wyznaczania
Wyznaczanie PDA jest zaimplementowane w funkcji SDK
create_program_address.
Algorytm działa następująco:
- Sprawdź, czy liczba seedów nie przekracza
MAX_SEEDS(16) i czy żaden pojedynczy seed nie przekraczaMAX_SEED_LEN(32 bajtów). Jeśli którykolwiek z tych warunków nie jest spełniony, zwróćPubkeyError::MaxSeedLengthExceeded. - Wykonaj haszowanie SHA-256 wszystkich seedów, identyfikatora programu oraz
ciągu
"ProgramDerivedAddress", aby uzyskać 32-bajtowy wynik. - Sprawdź, czy wynik jest poprawnym punktem na krzywej Ed25519.
- Jeśli wynik JEST na krzywej, zwróć
PubkeyError::InvalidSeeds(adres miałby odpowiadający klucz prywatny, co narusza bezpieczeństwo PDA). - Jeśli wynik NIE jest na krzywej, zwróć go jako PDA.
Koszty jednostek obliczeniowych
Wywołanie systemowe on-chain
dla create_program_address pobiera
1 500 CU
za każde wywołanie.
try_find_program_address syscall
pobiera 1 500 CU przy wejściu (przed pętlą), a następnie dodatkowe 1 500 CU za
każdą nieudaną próbę bumpowania w pętli.
Typowe wzorce seedów
Seedy są specyficzne dla aplikacji. Typowe wzorce to:
| Wzorzec | Seedy | Przypadek użycia |
|---|---|---|
| Globalny singleton | ["global"] | Globalne konto konfiguracyjne programu |
| Konto użytkownika | ["user", user_pubkey] | Jedno konto na użytkownika na program |
| Użytkownik-podmiot | ["vault", user_pubkey, mint_pubkey] | Skarbce tokenów, użytkownik-token |
| Licznik / sekwencyjne | ["order", user_pubkey, &order_id.to_le_bytes()] | Sekwencyjne rekordy na użytkownika |
Seedy są łączone przed haszowaniem, więc ["ab", "cd"] i ["abcd"] generują
to samo PDA. Aby uniknąć kolizji, używaj seedów o stałej długości lub
separatora. Na przykład, ["ab", "-", "cd"] jest jednoznaczny.
Przykłady: wyprowadzenie PDA
Wyprowadzenie PDA oblicza tylko adres. Nie tworzy konta on-chain pod tym
adresem. Konto musi zostać utworzone jawnie przez osobną instrukcję (zwykle
create_account przez CPI).
SDK Solana udostępniają funkcje do wyprowadzania PDA. Każda funkcja przyjmuje:
- Program ID: Adres programu używanego do wyprowadzenia PDA. Ten program może podpisywać w imieniu PDA.
- Opcjonalne seedy: Predefiniowane dane wejściowe, takie jak stringi, liczby lub inne adresy kont.
| SDK | Funkcja |
|---|---|
@solana/kit (TypeScript) | getProgramDerivedAddress |
@solana/web3.js (TypeScript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Poniższe przykłady pokazują, jak wyprowadzić PDA za pomocą SDK Solana. Kliknij ▷ Uruchom, aby wykonać kod.
Wyprowadzenie PDA z seed w postaci ciągu znaków
Poniższy przykład pokazuje, jak wyprowadzić PDA przy użyciu program ID oraz opcjonalnego 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 seed w postaci adresu
Poniższy przykład pokazuje, jak wyprowadzić PDA przy użyciu program ID oraz opcjonalnego 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 seedami
Poniższy przykład pokazuje, jak wyprowadzić PDA przy użyciu program ID oraz wielu opcjonalnych seed.
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}`);
Iteracja po wszystkich bumpach
Poniższe przykłady pokazują wyprowadzanie PDA przy użyciu wszystkich możliwych
bump seedów (od 255 do 0), ilustrując jak find_program_address zwraca
kanoniczny bump:
Przykład dla Kit nie jest dołączony, 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
W tym przykładzie bump 255 generuje adres na krzywej i kończy się niepowodzeniem. Pierwszy poprawny bump to 254, co czyni go kanonicznym bumpem.
Is this page helpful?