Adres pochodny programu
Adres konta w Solanie wskazuje lokalizację konta na blockchainie. Wiele adresów kont to klucze publiczne pary kluczy (keypair), w takim przypadku odpowiadający im klucz prywatny jest używany do podpisywania transakcji związanych z kontem.
Przydatną alternatywą dla adresu klucza publicznego jest adres pochodny programu (PDA). PDA oferują łatwą metodę przechowywania, mapowania i pobierania stanu programu. PDA to adres tworzony deterministycznie przy użyciu identyfikatora programu (program ID) oraz kombinacji opcjonalnych, zdefiniowanych wcześniej danych wejściowych. PDA wyglądają podobnie do adresów kluczy publicznych, ale nie mają odpowiadającego im klucza prywatnego.
Środowisko wykonawcze Solany umożliwia programom podpisywanie PDA bez potrzeby posiadania klucza prywatnego. Korzystanie z PDA eliminuje konieczność śledzenia adresu konta. Zamiast tego można przypomnieć sobie konkretne dane wejściowe użyte do pochodzenia PDA. (Aby dowiedzieć się, jak programy używają PDA do podpisywania, zobacz sekcję Wywołania międzyprogramowe.)
Tło
Pary kluczy Solany (keypairs) są punktami na krzywej Ed25519 (kryptografia krzywych eliptycznych). Składają się z klucza publicznego i klucza prywatnego. Klucz publiczny staje się adresem konta, a klucz prywatny jest używany do generowania ważnych podpisów dla konta.
Dwa konta z adresami na krzywej
PDA jest celowo pochodny, aby znajdować się poza krzywą Ed25519. Oznacza to, że nie ma ważnego odpowiadającego mu klucza prywatnego i nie może wykonywać operacji kryptograficznych (takich jak dostarczanie podpisu). Jednak Solana umożliwia programom podpisywanie PDA bez potrzeby posiadania klucza prywatnego.
Adres poza krzywą
Możesz myśleć o PDA jako o sposobie tworzenia struktur podobnych do hashmap na łańcuchu za pomocą zdefiniowanego zestawu wejść. (Na przykład ciągi znaków, liczby i inne adresy kont.)
Adres pochodny programu
Wyprowadzenie PDA
Przed utworzeniem konta z PDA musisz najpierw wyprowadzić adres. Wyprowadzenie PDA nie tworzy automatycznie konta w łańcuchu pod tym adresem — konto musi zostać wyraźnie utworzone za pomocą programu użytego do wyprowadzenia PDA. Możesz myśleć o PDA jak o adresie na mapie: sam fakt, że adres istnieje, nie oznacza, że coś tam zostało zbudowane.
SDK Solana obsługują tworzenie PDA za pomocą funkcji pokazanych w tabeli poniżej. Każda funkcja przyjmuje następujące dane wejściowe:
- ID programu: Adres programu używanego do wyprowadzenia PDA. Ten program może podpisywać się w imieniu PDA.
- Opcjonalne seedy: Zdefiniowane wcześniej dane wejściowe, takie jak ciągi znaków, liczby lub inne adresy kont.
| SDK | Funkcja |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Funkcja używa ID programu i opcjonalnych seedów, a następnie iteruje przez wartości bump, aby spróbować utworzyć prawidłowy adres programu. Iteracja wartości bump zaczyna się od 255 i zmniejsza o 1, aż zostanie znaleziony prawidłowy PDA. Po znalezieniu prawidłowego PDA funkcja zwraca PDA i bump seed.
Bump seed to dodatkowy bajt dołączany do opcjonalnych seedów, aby zapewnić wygenerowanie prawidłowego adresu poza krzywą.
Wyprowadzenie PDA
Kanoniczny bump
Bump seed to dodatkowy bajt dołączany do opcjonalnych seedów. Funkcja wyprowadzania 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ść, która generuje prawidłowy adres poza krzywą, nazywana jest "kanonicznym bumpem".
Poniższe przykłady pokazują wyprowadzenie 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
W tym przykładzie pierwszy bump seed powoduje błąd. Pierwszy bump seed, który wyprowadza prawidłowy PDA, to 254. Bump seedy od 253 do 251 również wyprowadzają unikalne, prawidłowe PDA.
Oznacza to, że przy tych samych opcjonalnych seedach i programId, bump seed o
innej wartości może nadal wyprowadzić prawidłowy PDA.
Zawsze uwzględniaj kontrole bezpieczeństwa, aby upewnić się, że PDA przekazany do programu jest wyprowadzony 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 wyprowadzania PDA.
Przykłady
Poniższe przykłady wyprowadzają PDA przy użyciu SDK Solana. Kliknij ▷ Uruchom, aby wykonać kod.
Wyprowadzenie PDA z seedem w postaci ciągu znaków
Poniższy przykład wyprowadza PDA przy użyciu identyfikatora programu i opcjonalnego seeda 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 wyprowadza PDA przy użyciu ID programu i 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 "seeds"
Poniższy przykład wyprowadza PDA przy użyciu ID programu i wielu opcjonalnych "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}`);
Utworzenie konta PDA
Poniższy przykład wykorzystuje
framework Anchor do utworzenia nowego konta
z adresem wyprowadzonym przez program. Program zawiera pojedynczą instrukcję
initialize do utworzenia nowego konta, które będzie
przechowywać adres użytkownika oraz
bump seed użyte do wyprowadzenia 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 bumpdaccount_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,}
Ograniczenie init instruuje Anchor, aby
wywołał System Program w celu
utworzenia nowego konta, używając PDA jako adresu. "Seeds" użyte do utworzenia
PDA to:
- Adres konta użytkownika podany w instrukcji
- Stały ciąg znaków: "data"
- Kanoniczny bump seed
W tym przykładzie ograniczenie "bump" nie jest przypisane do wartości, więc
Anchor użyje find_program_address do wyprowadzenia PDA i znalezienia "bump".
#[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 poniżej zawiera transakcję, która wywołuje instrukcję
initialize w celu utworzenia nowego konta z adresem
wyprowadzonym z programu. Plik zawiera kod do wyprowadzenia PDA.
Przykład pokazuje również, jak pobrać nowe konto, które zostanie utworzone.
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { PdaAccount } from "../target/types/pda_account";import { PublicKey } from "@solana/web3.js";describe("pda-account", () => {const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.PdaAccount as Program<PdaAccount>;const user = provider.wallet as anchor.Wallet;// Derive the PDA address using the seeds specified on the programconst [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});});
Jeśli ponownie wywołasz instrukcję initialize z tym samym seedem adresu
user, transakcja zakończy się niepowodzeniem. Dzieje się tak, ponieważ konto
już istnieje pod wyprowadzonym adresem.
Is this page helpful?