Program Derived Address (PDA)
Program Derived Addresses (PDAs) предоставляют разработчикам на Solana два основных варианта использования:
- Детерминированные адреса аккаунтов: PDA предоставляют механизм для детерминированного создания адреса с использованием комбинации опциональных "seeds" (предопределенных входных данных) и конкретного ID программы.
- Возможность подписи программой: Среда выполнения Solana позволяет программам "подписывать" PDA, которые получены из адреса программы.
Вы можете думать о PDA как о способе создания структур, подобных хеш-картам, в блокчейне из предопределенного набора входных данных (например, строк, чисел и других адресов аккаунтов).
Преимущество этого подхода в том, что он устраняет необходимость отслеживать точный адрес. Вместо этого вам просто нужно помнить конкретные входные данные, использованные для его получения.
Program Derived Address
Важно понимать, что простое получение Program Derived Address (PDA) не создает автоматически аккаунт в блокчейне по этому адресу. Аккаунты с PDA в качестве адреса в блокчейне должны быть явно созданы через программу, используемую для получения адреса. Вы можете думать о получении PDA как о поиске адреса на карте. Наличие адреса не означает, что по этому адресу что-то построено.
В этом разделе рассматриваются детали получения PDA. Раздел о Cross Program Invocations (CPIs) объясняет, как программы используют PDA для подписания.
Ключевые моменты
- PDA - это адреса, полученные детерминированно с использованием комбинации предопределенных seeds, bump seed и ID программы.
- PDA - это адреса, которые находятся вне кривой Ed25519 и не имеют соответствующего приватного ключа.
- Программы Solana могут подписывать от имени PDA, полученных из ID программы.
- Получение PDA не создает автоматически аккаунт в блокчейне.
- Аккаунт, использующий PDA в качестве своего адреса, должен быть создан через инструкцию внутри программы Solana.
Что такое PDA
PDA — это детерминированно выводимые адреса, которые выглядят как публичные ключи, но не имеют приватных ключей. Это означает, что невозможно сгенерировать действительную подпись для такого адреса. Однако среда выполнения Solana позволяет программам "подписывать" от имени PDA без необходимости в приватном ключе.
Для контекста, Solana Keypairs являются точками на кривой Ed25519 (эллиптическая криптография) с публичным ключом и соответствующим приватным ключом. Публичные ключи используются как адреса (уникальные идентификаторы) для аккаунтов в блокчейне.
Адрес на кривой
PDA — это точка, которая намеренно выводится так, чтобы не попадать на кривую Ed25519, используя предопределенный набор входных данных. Точка, не находящаяся на кривой Ed25519, не имеет действительного соответствующего приватного ключа и не может выполнять криптографические операции (подписание).
PDA может служить адресом (уникальным идентификатором) для аккаунта в блокчейне, предоставляя метод для легкого хранения, отображения и получения состояния программы.
Адрес вне кривой
Как вывести PDA
Для вывода PDA требуются три входных параметра:
- Опциональные seed: Предопределенные входные данные (например, строки, числа, другие адреса аккаунтов) для вывода PDA.
- Bump seed: Дополнительный байт, добавляемый к опциональным seed, чтобы гарантировать генерацию действительного PDA (вне кривой). Bump seed начинается с 255 и уменьшается на 1 до тех пор, пока не будет найден действительный PDA.
- Program ID: Адрес программы, от которой выводится PDA. Эта программа может подписывать от имени PDA.
Вывод PDA
Используйте следующие функции из соответствующих SDK для вывода PDA.
SDK | Функция |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
Для получения PDA предоставьте следующие входные данные в функцию SDK:
- Предопределенные опциональные seed, преобразованные в байты
- ID программы (адрес), используемый для получения
После нахождения действительного 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}`);
Получение 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 с несколькими опциональными seed
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 требуется "bump seed", дополнительный байт, добавляемый к опциональным seeds. Функция получения перебирает значения бампа, начиная с 255 и уменьшая на 1, пока значение не даст действительный адрес вне кривой. Первое значение бампа, которое дает действительный адрес вне кривой, является "каноническим бампом".
Следующие примеры показывают получение PDA с использованием всех возможных bump seeds (от 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 255 вызывает ошибку, и первым bump seed, который дает действительный PDA, является 254.
Обратите внимание, что bump seeds 253-251 все дают действительные PDA с разными
адресами. Это означает, что при одинаковых опциональных seeds и programId
,
bump seed с другим значением все равно может дать действительный PDA.
При создании программ Solana всегда включайте проверки безопасности, чтобы убедиться, что PDA, переданный в программу, получен из канонического бампа. Отсутствие таких проверок может создать уязвимости, позволяющие использовать неожиданные аккаунты в инструкциях программы. Лучшей практикой является использование только канонического бампа при получении PDA.
Создание аккаунтов PDA
Пример программы ниже показывает, как создать аккаунт, используя PDA в качестве адреса нового аккаунта. Пример программы использует фреймворк Anchor.
Программа включает одну 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 bumpaccount_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,}
В этом примере seed для получения PDA включают фиксированную строку data
и
адрес аккаунта user
, предоставленный в инструкции. Фреймворк Anchor
автоматически находит канонический seed bump
.
#[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 вызвать системную программу для создания
нового аккаунта, используя PDA в качестве адреса. Anchor делает это через
CPI.
#[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.
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
Транзакция в тестовом файле вызывает инструкцию initialize
для создания нового
аккаунта в сети, используя PDA в качестве адреса. В этом примере Anchor может
определить адрес PDA в аккаунтах инструкции, поэтому его не нужно указывать
явно.
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
более одного раза, используя тот же адрес user
в качестве seed, транзакция
завершится с ошибкой. Это происходит потому, что аккаунт по полученному адресу
уже существует.
Is this page helpful?