PDA (Program Derived Address) — це механізм Solana, який дозволяє програмі володіти акаунтами та підписувати від їхнього імені без приватного ключа. Нижче — системне пояснення того, як це працює, без зайвих абстракцій.

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

Що таке PDA

Program Derived Address — адреса без приватного ключа

PDA — це адреса в мережі Solana, яка математично виводиться з вашої програми та набору seed-значень. Її головна відмінність від звичайного гаманця: у PDA немає приватного ключа. Це не обмеження, а навмисна властивість.

Звичайна адреса в Solana належить ключовій парі: приватний ключ генерує публічний через криву ed25519. PDA ж обчислюється так, щоб отриманий публічний ключ не відповідав жодному приватному ключу на цій кривій. Такого приватного ключа фізично не існує.

Чому PDA не може підписувати транзакції

Оскільки приватного ключа немає, жоден зовнішній агент — користувач, гаманець чи інша програма — не може підписати транзакцію від імені PDA. Єдиний спосіб створити валідний підпис від імені PDA: сама програма, яка цей PDA згенерувала, викликає invoke_signed і передає правильні seed-значення. Runtime Solana перевіряє, що програма дійсно є власником цієї адреси, і умовно "підписує" інструкцію.

Як генерується PDA

seeds, program_id, bump

Базова формула: хешування SHA-256 від конкатенації seed-значень та program_id. Але не кожен результат SHA256 потрапляє на криву ed25519. Більшість хешів розташовані поза кривою (off-curve) — і саме такі нам потрібні.

Якщо хеш випадково потрапляє на криву, Solana додає до вхідних даних додатковий байт — bump seed — і повторює хешування. Bump починається з 255 і зменшується на 1 кожну ітерацію, поки не буде знайдено off-curve адресу.

Фінальна адреса PDA однозначно визначається трьома складовими:

  • seeds — набір значень (рядки, байти, числа, публічні ключі), які ви обираєте як логічний ідентифікатор акаунта
  • program_id — публічний ключ програми, що генерує PDA
  • bump — числове значення, яке гарантує off-curve результат

findProgramAddressSync — пошук PDA

На практиці ви не обчислюєте хеш вручну. У нативному Rust для Solana та в Anchor є функція findProgramAddressSync:

Середовище: Solana CLI 1.18.x, Anchor 0.30.x, Rust 1.75+, кластер Devnet

let seeds = &[b"user_state", user_pubkey.as_ref()];
let (pda, bump) = Pubkey::find_program_address(seeds, &program_id);

Функція повертає кортеж із двох значень: саму адресу PDA та знайдений bump. Bump обов'язково треба зберегти в акаунт-даних, щоб пізніше використати при підписанні через invoke_signed.

Навіщо потрібен PDA

Детерміноване зберігання даних програми

PDA дозволяє програмі мати власні акаунти для зберігання стану. Наприклад, якщо ви створюєте торговельну платформу, кожна лістинг-заявка може мати свій PDA-акаунт, де seed-ами виступають ідентифікатор товару та адреса продавця:

let seeds = &[b"listing", listing_id.to_le_bytes().as_ref(), seller.as_ref()];
let (pda, bump) = Pubkey::find_program_address(seeds, &program_id);

Ключова перевага: ви завжди можете відновити адресу акаунта, знаючи seed-значення. Не треба зберігати адресу окремо або шукати її в списку — вона детерміновано обчислюється. Це усуває цілий клас помилок, пов'язаних із втратою посилань на акаунти.

Унікальність: неможливо підробити адресу

Оскільки PDA залежить від program_id, жодна інша програма не може згенерувати ту саму адресу з тими самими seed-значеннями. Це гарантує ексклюзивний контроль: лише ваша програма може довести власність над цим PDA. Навіть якщо сторонній розробник знає ваші seed-значення, він не зможе створити акаунт за цією адресою, бо не зможе надати правильний підпис — Runtime відхилить транзакцію.

PDA як signer

invoke_signed — підписання від імені PDA

Коли ваша програма викликає іншу програму через CPI (Cross-Program Invocation) і потрібно, щоб PDA фігурував як signer, використовується invoke_signed:

let signer_seeds = &[
    b"vault",
    authority.as_ref(),
    &[bump],
];

invoke_signed(
    &transfer_instruction,
    &[from_account, to_account, system_program],
    &[signer_seeds],
)?;

Останній аргумент — масив signer_seeds — містить ті самі seed-значення та bump, за якими було знайдено PDA. Runtime Solana обчислює адресу з цих даних та program_id. Якщо результат збігається з переданою адресою — підпис вважається валідним.

Коли програма підписує від імені PDA

Типові сценарії, де PDA виступає signer:

  • Перерахування токенів з PDA-акаунта — наприклад, виплата виграшу в грі або видача позики
  • Закриття PDA-акаунта та повернення SOL на адресу ініціатора транзакції
  • Передача NFT з escrow-акаунта, який належить програмі через PDA

Важливе обмеження: програма може підписувати від імені PDA лише під час виконання своєї інструкції. Після завершення інструкції PDA знову "безголосий" — жоден зовнішній підпис неможливий до наступного виклику програми.

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

Seeds не відповідають (invalid seeds)

Найчастіша помилка: seed-значення при пошуку PDA не збігаються з тими, що використовувалися при створенні акаунта. Приклади:

  • Ви зберегли рядок "user_wallet" як seed, а при пошуку передаєте байти без роздільника між seed-ами
  • Порядок seed-значень змінився між створенням акаунта та його використанням
  • Числовий seed серіалізовано як big-endian замість little-endian або навпаки

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

Bump seed невірний

Якщо ви зберегли bump у акаунт-даних, але при invoke_signed передали інше значення — підпис буде невалідним. Runtime обчислить іншу адресу, вона не збігається з переданою, і транзакція впаде з помилкою підпису.

Інший поширений випадок: ви хардкодите bump як 255 замість того, щоб використовувати збережене значення. Це спрацює лише якщо bump дійсно дорівнює 255, що трапляється рідко. Завжди використовуйте саме той bump, який повернула findProgramAddressSync при створенні акаунта.

Наступний крок у вивченні — практичне створення PDA у програмі. Перейдіть до розділу Як створити PDA у програмі, щоб побудувати перший робочий приклад на Devnet.

Джерела