Program Derived Address (PDA)

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

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

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

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

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

Получение 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}`);
Click to execute the code.

Канонический бамп

Для получения 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);
}
}
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 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 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 вызвать системную программу для создания нового аккаунта, используя 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?