Адрес, производный от программы
Адрес аккаунта в Solana указывает на местоположение аккаунта в блокчейне. Многие адреса аккаунтов являются открытым ключом ключевой пары, в этом случае соответствующий закрытый ключ используется для подписания транзакций, связанных с аккаунтом.
Полезной альтернативой адресу открытого ключа является адрес, производный от программы (PDA). PDA предоставляет простой способ хранения, сопоставления и извлечения состояния программы. PDA — это адрес, который создается детерминированно с использованием идентификатора программы и комбинации опциональных предопределенных входных данных. PDA выглядят как адреса открытых ключей, но не имеют соответствующего закрытого ключа.
Среда выполнения Solana позволяет программам подписывать PDA без необходимости в закрытом ключе. Использование PDA устраняет необходимость отслеживать адрес аккаунта. Вместо этого вы можете вспомнить конкретные входные данные, использованные для создания PDA. (Чтобы узнать, как программы используют PDA для подписания, см. раздел Вызовы между программами.)
Основы
Ключевые пары Solana (keypairs) являются точками на кривой Ed25519 (эллиптическая криптография). Они состоят из открытого ключа и закрытого ключа. Открытый ключ становится адресом аккаунта, а закрытый ключ используется для создания действительных подписей для аккаунта.
Два аккаунта с адресами на кривой
PDA намеренно создается так, чтобы находиться вне кривой Ed25519. Это означает, что он не имеет действительного соответствующего закрытого ключа и не может выполнять криптографические операции (например, предоставлять подпись). Однако Solana позволяет программам подписывать PDA без необходимости в закрытом ключе.
Адрес вне кривой
Вы можете рассматривать PDAs как способ создания структур, похожих на хэш-таблицы, в блокчейне с использованием заранее определённого набора входных данных (например, строк, чисел и других адресов аккаунтов).
Адрес, производный от программы
Получение PDA
Прежде чем создать аккаунт с PDA, сначала необходимо получить адрес. Получение PDA не создаёт автоматически аккаунт в блокчейне по этому адресу — аккаунт должен быть явно создан через программу, использованную для получения PDA. Вы можете представить PDA как адрес на карте: просто потому, что адрес существует, это не означает, что там что-то построено.
SDK Solana поддерживают создание PDA с помощью функций, указанных в таблице ниже. Каждая функция принимает следующие входные данные:
- ID программы: Адрес программы, используемой для получения PDA. Эта программа может подписывать от имени PDA.
 - Необязательные seeds: Заранее определённые входные данные, такие как строки, числа или другие адреса аккаунтов.
 
| SDK | Функция | 
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress | 
@solana/web3.js (Typescript) | findProgramAddressSync | 
solana_sdk (Rust) | find_program_address | 
Функция использует ID программы и необязательные seeds, затем перебирает значения bump, чтобы попытаться создать действительный адрес программы. Перебор значений bump начинается с 255 и уменьшается на 1 до тех пор, пока не будет найден действительный PDA. После нахождения действительного PDA функция возвращает PDA и bump seed.
Bump seed — это дополнительный байт, добавляемый к необязательным seeds, чтобы гарантировать генерацию действительного адреса вне кривой.
Вывод PDA
Канонический bump
Bump seed — это дополнительный байт, добавляемый к необязательным seeds. Функция вывода перебирает значения bump, начиная с 255 и уменьшая на 1, пока не будет найдено значение, которое создаёт корректный off-curve адрес. Первое значение, которое создаёт корректный off-curve адрес, называется "каноническим bump."
Следующие примеры показывают вывод PDA с использованием всех возможных bump seed (от 255 до 0):
Пример с Kit не включён, так как функция createProgramDerivedAddress не экспортируется.
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
В этом примере первый bump seed вызывает ошибку. Первый bump seed, который выводит корректный PDA, — это 254. Bump seed с 253 по 251 также вычисляют уникальные корректные PDA.
Это означает, что при использовании одинаковых необязательных seeds и
programId bump seed с разным значением всё равно может вычислить корректный
PDA.
Всегда включайте проверки безопасности, чтобы убедиться, что PDA, переданный в программу, выведен из канонического bump. Отсутствие таких проверок может привести к уязвимостям, позволяющим использовать неожиданные аккаунты в инструкциях программы. Рекомендуется использовать только канонический bump при выводе PDA.
Примеры
Примеры ниже показывают вывод PDA с использованием SDK Solana. Нажмите ▷ Run, чтобы выполнить код.
Вывод PDA с использованием строкового seed
Пример ниже выводит PDA с использованием ID программы и необязательного строкового seed.
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}`);
Вывод PDA с адресным seed
Пример ниже выводит PDA, используя идентификатор программы и необязательный адресный seed.
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}`);
Вывод PDA с несколькими seeds
Пример ниже выводит PDA, используя идентификатор программы и несколько необязательных 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}`);
Создание аккаунта PDA
Пример ниже использует фреймворк Anchor для
создания нового аккаунта с адресом, выведенным программой. Программа включает
единственную инструкцию initialize для создания нового
аккаунта, который будет хранить адрес пользователя и
bump seed, использованные для вывода 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,}
Ограничение init указывает Anchor
вызвать System Program для создания
нового аккаунта, используя PDA в качестве адреса. Seeds, использованные для
создания PDA, включают:
- Адрес аккаунта пользователя, предоставленный в инструкции
 - Фиксированную строку: "data"
 - Канонический bump seed
 
В этом примере ограничение bump не назначено значению, поэтому Anchor использует
find_program_address для вывода PDA и нахождения bump.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
Тестовый файл ниже содержит транзакцию, которая вызывает инструкцию
initialize для создания нового аккаунта с адресом,
производным от программы. Файл содержит код для вывода PDA.
Пример также показывает, как получить новый аккаунт, который будет создан.
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));});});
Если вы снова вызовете инструкцию initialize с тем же seed адреса user,
транзакция завершится с ошибкой. Это происходит, потому что аккаунт уже
существует по вычисленному адресу.
Is this page helpful?