Program Derived Address (PDA)

Program Derived Addresses (PDA) предоставляют разработчикам на Solana два основных применения:

  • Детерминированные адреса аккаунтов: PDA предоставляют механизм для детерминированного создания адреса с использованием комбинации необязательных "seed" (предопределённых входных данных) и определённого ID программы.
  • Обеспечение подписи программ: Среда выполнения Solana позволяет программам "подписывать" PDA, которые были созданы на основе адреса программы.

PDA можно рассматривать как способ создания структур, похожих на хэш-таблицы, в блокчейне на основе предопределённого набора входных данных (например, строк, чисел и других адресов аккаунтов).

Преимущество такого подхода заключается в том, что отпадает необходимость отслеживать точный адрес. Вместо этого достаточно помнить конкретные входные данные, использованные для его создания.

Program Derived AddressProgram Derived Address

Важно понимать, что простое создание Program Derived Address (PDA) не означает автоматического создания аккаунта в блокчейне по этому адресу. Аккаунты с PDA в качестве адреса в блокчейне должны быть явно созданы через программу, использованную для создания адреса. Это можно сравнить с нахождением адреса на карте. Наличие адреса не означает, что по этому адресу что-то построено.

Этот раздел охватывает детали создания PDA. Раздел о Cross Program Invocations (CPI) объясняет, как программы используют PDA для подписей.

Основные моменты

  • PDA — это адреса, создаваемые детерминированно с использованием комбинации предопределённых seed, bump seed и ID программы.
  • PDA — это адреса, которые находятся за пределами кривой Ed25519 и не имеют соответствующего приватного ключа.
  • Программы Solana могут подписывать от имени PDA, созданных на основе их ID программы.
  • Создание PDA не означает автоматического создания аккаунта в блокчейне.
  • Аккаунт с PDA в качестве адреса должен быть создан через инструкцию внутри программы Solana.

Что такое PDA

PDA — это адреса, которые детерминированно выводятся и выглядят как открытые ключи, но не имеют закрытых ключей. Это означает, что невозможно сгенерировать действительную подпись для такого адреса. Однако среда выполнения Solana позволяет программам "подписывать" PDA без необходимости использования закрытого ключа.

Для контекста: Keypairs в Solana — это точки на кривой Ed25519 (эллиптическая криптография) с открытым ключом и соответствующим закрытым ключом. Открытые ключи используются в качестве адресов (уникальных идентификаторов) для аккаунтов в блокчейне.

Адрес на кривойАдрес на кривой

PDA — это точка, которая намеренно выводится так, чтобы находиться за пределами кривой Ed25519, используя заранее определённый набор входных данных. Точка, которая не находится на кривой Ed25519, не имеет действительного соответствующего закрытого ключа и не может выполнять криптографические операции (подпись).

PDA может служить адресом (уникальным идентификатором) для аккаунта в блокчейне, предоставляя метод для удобного хранения, сопоставления и извлечения состояния программы.

Адрес вне кривойАдрес вне кривой

Как вывести PDA

Для вывода PDA требуются три входных параметра:

  • Необязательные seeds: Заранее определённые входные данные (например, строки, числа, другие адреса аккаунтов) для вывода PDA.
  • Bump seed: Дополнительный байт, добавляемый к необязательным seeds, чтобы гарантировать генерацию действительного PDA (вне кривой). Bump seed начинается с 255 и уменьшается на 1, пока не будет найден действительный PDA.
  • Program ID: Адрес программы, из которой выводится PDA. Эта программа может подписывать от имени PDA.

Вывод PDAВывод PDA

Используйте следующие функции из соответствующих SDK для вывода PDA.

SDKФункция
@solana/kit (Typescript)getProgramDerivedAddress
@solana/web3.js (Typescript)findProgramAddressSync
solana_sdk (Rust)find_program_address

Чтобы вывести PDA, предоставьте следующие входные данные функции SDK:

  • Предопределённые необязательные seeds, преобразованные в байты
  • Идентификатор программы (адрес), используемый для вывода

После нахождения допустимого PDA функция возвращает как адрес (PDA), так и bump seed, использованный для вывода.

Примеры

Следующие примеры показывают, как вывести PDA с использованием соответствующих SDK.

Нажмите кнопку "Выполнить", чтобы выполнить код.

Вывод PDA с необязательным строковым 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}`);
Console
Click to execute the code.

Вывод 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}`);
Console
Click to execute the code.

Вывод 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}`);
Console
Click to execute the code.

Канонический bump

Для вычисления PDA требуется "bump seed" — дополнительный байт, добавляемый к необязательным seeds. Функция вычисления перебирает значения bump, начиная с 255 и уменьшая на 1, пока не будет найдено значение, которое создаёт корректный off-curve адрес. Первое значение bump, которое создаёт корректный 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);
}
}
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 вызывает ошибку, и первым bump seed, который вычисляет корректный PDA, является 254.

Обратите внимание, что bump seed с 253 по 251 также вычисляют корректные PDA с разными адресами. Это означает, что при использовании одинаковых необязательных seeds и programId bump seed с разным значением всё равно может вычислить корректный PDA.

При создании программ на Solana всегда включайте проверки безопасности, чтобы убедиться, что PDA, переданный в программу, вычислен из канонического bump. Отсутствие таких проверок может привести к уязвимостям, позволяющим использовать неожиданные аккаунты в инструкциях программы. Рекомендуется использовать только канонический bump при вычислении PDA.

Создание PDA аккаунтов

Пример программы ниже показывает, как создать аккаунт, используя PDA в качестве адреса нового аккаунта. Пример программы использует Anchor framework.

Программа включает одну инструкцию initialize для создания нового аккаунта, используя PDA в качестве адреса аккаунта. Новый аккаунт хранит адрес user и 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 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,
}

В этом примере seed для вычисления PDA включает фиксированную строку data и адрес аккаунта user, предоставленный в инструкции. Фреймворк Anchor автоматически находит канонический 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>,

Ограничение init указывает Anchor вызвать System Program для создания нового аккаунта, используя PDA в качестве адреса. Anchor делает это через 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>,

Тестовый файл содержит код на Typescript для вычисления PDA.

Derive PDA
const [PDA] = PublicKey.findProgramAddressSync(
[Buffer.from("data"), user.publicKey.toBuffer()],
program.programId
);

Транзакция в тестовом файле вызывает инструкцию initialize для создания нового аккаунта в блокчейне, используя PDA в качестве адреса. В этом примере Anchor может вывести адрес PDA в учетных записях инструкции, поэтому его не нужно указывать явно.

Invoke Initialize Instruction
it("Is initialized!", async () => {
const transactionSignature = await program.methods
.initialize()
.accounts({
user: user.publicKey
})
.rpc();
console.log("Transaction Signature:", transactionSignature);
});

Тестовый файл также показывает, как получить аккаунт в блокчейне, созданный по этому адресу, после отправки транзакции.

Fetch Account
it("Fetch Account", async () => {
const pdaAccount = await program.account.dataAccount.fetch(PDA);
console.log(JSON.stringify(pdaAccount, null, 2));
});

Обратите внимание, что в этом примере, если вы вызовете инструкцию initialize более одного раза, используя тот же адрес user в качестве seed, то транзакция завершится с ошибкой. Это происходит, потому что аккаунт уже существует по вычисленному адресу.

Is this page helpful?