Ця інструкція покаже, як створити PDA (Program Derived Address) у програмі на Anchor, зберегти bump для подальшого використання, знайти існуючий PDA та застосувати його для зберігання стану. Усі приклади перевірені на Anchor 0.30.x, Solana CLI 1.18.x, кластер — Devnet.

Версії для прикладів у «Як створити PDA у програмі» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.

Визначення seeds

Seeds — це масив байтів, який разом із ідентифікатором програми детерміновано визначає адресу PDA. Програма не має приватного ключа від цього акаунта, але може підписувати транзакції від його імені через invoke_signed.

Які дані використовувати як seeds

Seeds мають бути унікальними в межах вашої програми та передбачуваними для клієнтської частини, щоб знайти адресу без запиту до блокчейну. Типові варіанти:

  • Статичний байтовий літерал — для глобальних акаунтів програми, наприклад b"config".
  • Публічний ключ користувача — для акаунтів, прив'язаних до конкретної особи: user.key().as_ref().
  • Комбінація кількох значень — коли одного seed недостатньо для унікальності: b"vault", mint.key().as_ref().
  • Лічильник або ідентифікатор — для впорядкованих списків: b"item", &index.to_le_bytes().

Не використовуйте як seeds дані, які можуть змінюватися після створення акаунта (баланс, timestamp). Це порушить детермінованість.

Детермінованість: одні й ті ж seeds → та сама адреса

Функція Pubkey::find_program_address повертає кортеж із адреси та bump. Адреса залежить виключно від seeds та program_id. Якщо ви передасте ті самі seeds у тому самому порядку, отримаєте ту саму адресу — незалежно від того, чи акаунт уже створено на блокчейні.

Pubkey::find_program_address(&[b"config"], &program_id)
// поверне (address, bump) — завжди однакові для цих вхідних даних

Bump — це однобайтове значення (0–255), яке runtime підбирає так, щоб результуюча адреса не мала відповідного приватного ключа в кривій ed25519. Саме тому PDA не можна створити локально без ітерації — runtime виконує її за вас.

Створення PDA через Anchor

init constraint з seeds та bump

Anchor автоматизує створення PDA через атрибут init. Розглянемо структуру для створення глобального конфігураційного акаунта:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::pubkey::Pubkey;

declare_id!("11111111111111111111111111111111");

#[account]
pub struct Config {
  pub authority: Pubkey,
  pub bump: u8,
}

#[derive(Accounts)]
pub struct InitializeConfig<'info> {
  #[account(
    init,
    payer = authority,
    space = 8 + 32 + 1,
    seeds = [b"config"],
    bump
  )]
  pub config: Account<'info, Config>,

  #[account(mut)]
  pub authority: Signer<'info>,

  pub system_program: Program<'info, System>,
}

pub fn initialize_config(ctx: Context<InitializeConfig>) -> Result<()> {
  let config = &mut ctx.accounts.config;
  config.authority = ctx.accounts.authority.key();
  config.bump = ctx.bumps.config;
  Ok(())
}

Що відбувається при виклику цієї інструкції:

  1. Anchor обчислює PDA за seeds [b"config"] та program_id.
  2. Перевіряє, що акаунт за цією адресою ще не існує.
  3. Створює акаунт із розміром 41 байт (8 — дискримінатор Anchor, 32 — Pubkey, 1 — u8).
  4. Оплачує створення з акаунта authority.
  5. Встановлює owner створеного акаунта на вашу програму.
  6. Значення ctx.bumps.config містить знайдений bump — ви можете зберегти його в дані акаунта.

Збереження bump у даних акаунта

Збереження bump у самому акаунті — рекомендована практика. Це дозволяє клієнту не передавати bump у кожну транзакцію і гарантує, що програма використовує саме той bump, за яким акаунт було створено.

У прикладі вище поле bump: u8 у структурі Config зберігає це значення. Запис відбувається в тілі інструкції: config.bump = ctx.bumps.config;

Для акаунтів користувача з динамічними seeds логіка аналогічна:

#[account(
  init,
  payer = user,
  space = 8 + 32 + 1,
  seeds = [b"profile", user.key().as_ref()],
  bump
)]
pub profile: Account<'info, UserProfile>,

Тут seeds включають публічний ключ користувача, тому кожен користувач отримує унікальний PDA-профіль.

Знаходження існуючого PDA

constraint з seeds та bump

Коли акаунт уже створено, ви використовуєте mut замість init і перевіряєте seeds разом із bump:

#[derive(Accounts)]
pub struct UpdateConfig<'info> {
  #[account(
    mut,
    seeds = [b"config"],
    bump = config.bump
  )]
  pub config: Account<'info, Config>,

  pub authority: Signer<'info>,
}

pub fn update_config(ctx: Context<UpdateConfig>, new_authority: Pubkey) -> Result<()> {
  require!(
    ctx.accounts.config.authority == ctx.accounts.authority.key(),
    MyError::Unauthorized
  );
  ctx.accounts.config.authority = new_authority;
  Ok(())
}

Конструкція bump = config.bump каже Anchor: «використай збережений bump замість повторного обчислення». Це ефективніше, ніж bump без значення, яка змушує runtime шукати bump ітерацією щоразу.

Перевірка, що PDA належить програмі

Anchor виконує цю перевірку автоматично. Коли ви вказуєте seeds у макросі #[account(...)], фреймворк обчислює очікувану адресу і порівнює її з переданим акаунтом. Якщо адреси не збігаються — транзакція відхиляється з помилкою ConstraintSeeds.

Вам не потрібно вручну перевіряти config.owner == program_id — це робить Anchor. Але якщо ви працюєте з непідтримуваними типами акаунтів (наприклад, AccountInfo замість Account), перевірку owner доведеться додати самостійно через constraint.

Використання PDA для зберігання стану

Глобальний стан програми (config account)

PDA з фіксованими seeds підходить для зберігання конфігурації, яка єдина для всієї програми. Типовий приклад — акаунт адміністратора, комісія, статус паузи:

#[account]
pub struct Config {
  pub authority: Pubkey,
  pub fee_bps: u16,
  pub paused: bool,
  pub bump: u8,
}

Клієнт знаходить цей акаунт до відправки транзакції:

const CONFIG_SEEDS = [Buffer.from("config")];
const [configPda] = PublicKey.findProgramAddressSync(
  CONFIG_SEEDS,
  programId
);

Оскільки seeds відомі заздалегідь, клієнт завжди може обчислити адресу без читання з блокчейну.

Дані користувача (user profile account)

PDA з динамічними seeds зручно використовувати для профілів користувачів, позицій у стейкінгу, індивідуальних сховищ:

#[account]
pub struct UserProfile {
  pub user: Pubkey,
  pub username: String,
  pub created_at: i64,
  pub bump: u8,
}

#[derive(Accounts)]
pub struct CreateProfile<'info> {
  #[account(
    init,
    payer = user,
    space = 8 + 32 + 4 + 32 + 8 + 1,
    seeds = [b"profile", user.key().as_ref()],
    bump
  )]
  pub profile: Account<'info, UserProfile>,

  #[account(mut)]
  pub user: Signer<'info>,

  pub system_program: Program<'info, System>,
}

Клієнт обчислює адресу профілю, передаючи власний публічний ключ як частину seeds:

const [profilePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("profile"), userPublicKey.toBuffer()],
  programId
);

Це гарантує, що один користувач не може створити два профілі з однаковими seeds, а інший користувач не може підмінити акаунт — адреса жорстко прив'язана до ключа.

Типові помилки

PDA вже існує при спробі init

Помилка account already in use виникає, коли ви викликаєте інструкцію з init, а акаунт за обчисленою PDA-адресою вже існує на блокчейні. Причини:

  • Подвійний виклик — клієнт відправив дві однакові транзакції. Додайте ідімпотентність на стороні клієнта.
  • Невірні seeds — ви змінили seeds у коді, але акаунт зі старими seeds досі існує. Перевірте, які seeds використовувалися при першому створенні.
  • Тестування без скидання — між запусками тестів ви не скидали локальний ledger. Використовуйте solana-test-validator --reset.

Якщо акаунт має бути перестворений, його треба спочатку закрити через close constraint, який поверне rent на вказаний акаунт.

Невірний owner PDA

Помилка AccountOwnedByWrongProgram або ConstraintOwner означає, що акаунт за переданою адресою існує, але його owner — не ваша програма. Можливі причини:

  • Клієнт передав неправильну адресу — обчислення PDA на клієнті відрізняється від серверного. Перевірте порядок seeds і program_id з обох боків.
  • Акаунт створено іншою програмою — ті самі seeds з іншим program_id дають різні адреси, але випадкова збіга адреси з акаунтом іншої програми викликає цю помилку.
  • Використано AccountInfo без перевірки owner — якщо ви працюєте з сирими типами, додайте constraint = config.owner == program_id.

Для діагностики на Devnet виконайте:

solana account <PDA_ADDRESS> --url devnet

Перевірте поле Owner у виводі — воно має збігатися з program_id вашої програми.

Наступний крок у вивченні архітектури Solana — зрозуміти, як програма може підписувати транзакції від імені створеного PDA. Це тема наступного розділу: Що таке CPI у Solana.

Джерела