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 krzywejDwa 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ą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 programuAdres 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.
SDKFunkcja
@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 PDAWyprowadzenie 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);
}
}
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

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

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

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

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.

Program
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 bumpd
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,
}

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

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

Test
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 program
const [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?

Spis treści

Edytuj stronę

Zarządzane przez

© 2025 Solana Foundation.
Wszelkie prawa zastrzeżone.
Bądź na bieżąco