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

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

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

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);
}
}
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 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 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, 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.

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 Typescript do wyznaczenia 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 seeda, transakcja zakończy się niepowodzeniem. Dzieje się tak, ponieważ konto już istnieje pod wyznaczonym adresem.

Is this page helpful?

Spis treści

Edytuj stronę