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 AddressProgram 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 krzywejAdres 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ą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 PDAWyprowadzenie PDA

Użyj następujących funkcji z odpowiednich SDK, aby wyprowadzić PDA.

SDKFunkcja
@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}`);
Console
Click to execute the code.

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}`);
Console
Click to execute the code.

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}`);
Console
Click to execute the code.

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);
}
}
Console
Click to execute the code.
bump 255: Error: Invalid seeds, address must fall off the curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 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 bump
account_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 PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
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.

pda_account
#[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.

pda_account
#[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.

Derive 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.

Invoke Initialize Instruction
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.

Fetch Account
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?

Spis treści

Edytuj stronę