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

Constraints у Anchor — це декларативні правила, які фреймворк перевіряє до виконання логіки вашої функції. Вони замінюють десятки рядків ручних перевірок акаунтів і гарантують, що до інструкції потраплять лише коректні дані. Нижче — повний довідник з усіх основних обмежень, приклади для Devnet і розбір типових помилок.

Середовище: Anchor 0.30.1, Solana CLI 1.18.26, Rust 1.75.0+, кластер Devnet.

Що таке constraints

Декларативні правила для акаунтів

Кожен акаунт у структурі контексту інструкції (`Context<t>`) може бути супроводжений атрибутом `#[account(...)]`. Усі параметри всередині дужок — це constraints. Anchor генерує з них код перевірки на етапі компіляції та виконує його під час транзакції. Якщо хоча б один constraint не виконується, транзакція відхиляється з помилкою `ConstraintViolation` ще до входу в тіло функції.</t>

Це означає, що ваш бізнес-код у функції отримує вже перевірені акаунти з гарантованими властивостями.

Чому constraints замість ручних перевірок

У чистому Solana-програмуванні на Rust без Anchor ви маєте самостійно перевіряти: чи є акаунт signer, чи належить він правильному програму, чи збігаються ключі, чи достатньо місця. Кожна перевірка — це окремий виклик `solana_program::program_error::ProgramError` і ручне десеріалізування. Це призводить до повторюваного коду та високого ризику пропустити перевірку.

Anchor вирішує це так:

  • Декларативність: правила записані поруч із визначенням акаунта — одразу видно, що очікується.
  • Автоматична десеріалізація: якщо constraint пройшов, Anchor вже перетворив байти акаунта на Rust-структуру.
  • Єдина точка відмови: помилка `ConstraintViolation` з кодом конкретного constraint дає зрозуміти, що саме не спрацювало.

Основні constraints

Нижче наведено ключові constraints з прикладами. Усі приклади використовують спільну структуру даних:

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct Vault {
  pub authority: Pubkey,
  pub amount: u64,
}

mut

Вказує, що програма може змінювати дані акаунта. Без цього constraint Anchor передає акаунт як незмінне посилання, і спроба запису призведе до помилки компіляції.

#[account(mut)]
pub vault: Account<'info, Vault>,

Очікуваний результат: програма може виконати `vault.amount += 100;` без помилок компіляції та виконання.

signer

Гарантує, що акаунт підписав поточну транзакцію. Зазвичай застосовується до `Signer<'info>` або `AccountInfo<'info>`.

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

Очікуваний результат: якщо транзакцію підписав інший ключ, інструкція завершиться з `ConstraintSigner`.

seeds

Вказує насіння (seeds) для похідного адреси (PDA). Використовується разом із `bump` для перевірки, що переданий акаунт дійсно є цим PDA.

#[account(
  seeds = [b"vault", authority.key().as_ref()],
  bump
)]
pub vault: Account<'info, Vault>,

Очікуваний результат: Anchor обчислить PDA за вказаними seeds і програмою, порівняє з переданим ключем акаунта. Якщо не збігається — `ConstraintSeeds`.

payer

Вказує, який акаунт оплачує створення нового акаунта (rent-exempt резерв). Застосовується лише разом із `init`.

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

Очікуваний результат: з акаунта `authority` буде списано SOL на rent-exempt депозит для нового акаунта `vault`.

space

Визначає розмір акаунта в байтах. Застосовується лише з `init`. Формула: 8 байтів (дискримінатор Anchor) + сума розмірів усіх полів структури.

Для `Vault` з `Pubkey` (32 байти) та `u64` (8 байтів): 8 + 32 + 8 = 48.

space = 8 + 32 + 8

Очікуваний результат: акаунт буде створений із рівно 48 байтами. Якщо вказати менше — десеріалізація впаде. Більше — марна трата SOL.

owner

Перевіряє, що акаунт належить вказаній програмі. Корисний для `AccountInfo<'info>`, коли ви не десеріалізуєте акаунт через `Account<'info, T>`, але хочете переконатися у власнику.

#[account(owner = system_program.key())]
pub system_account: AccountInfo<'info>,

Очікуваний результат: якщо акаунт належить іншій програмі — `ConstraintOwner`.

Constraints для безпеки

constraint = <вираз> — користувацькі умови

Найгнучкіший constraint. Дозволяє записати будь-який логічний вираз, що повертає `bool`. Виконується після усіх інших перевірок цього акаунта.

#[account(
  mut,
  seeds = [b"vault", authority.key().as_ref()],
  bump,
  constraint = vault.authority == authority.key()
)]
pub vault: Account<'info, Vault>,

Очікуваний результат: інструкція виконається лише якщо поле `authority` у даних vault збігається з ключем підписанта. Інакше — `ConstraintConstraintViolation`.

Можна комбінувати кілька умов через `&&` та `||`:

constraint = vault.amount > 0 && vault.amount <= 1_000_000_000

Застереження: у production-програмах складні вирази в `constraint` збільшують розмір програми та час виконання. Для критичної логіки з багатьма гілками краще виконувати перевірки в тілі функції з власними кодами помилок.

Перевірка власника акаунта (owner constraint)

Крім явного `owner = ...`, Anchor автоматично перевіряє власника при використанні `Account<'info, T>`. Якщо акаунт не належить поточній програмі, ви отримаєте `AccountOwnedByWrongProgram` ще до перевірки ваших constraints.

Тому `owner` зазвичай потрібен лише для `AccountInfo<'info>` — коли ви працюєте з «чужими» акаунтами (System Program, Token Program, Clock тощо).

#[account(owner = token::ID)]
pub mint: AccountInfo<'info>,

Constraints для PDA

seeds та bump — зв'язування PDA

PDA (Program Derived Address) — це адреса, детерміновано обчислена з програми та набору seeds. У Anchor є два способи роботи з bump:

1. Автоматичний bump (рекомендований для початківців):

#[account(
  seeds = [b"vault", authority.key().as_ref()],
  bump
)]
pub vault: Account<'info, Vault>,

Anchor сам знаходить коректний bump, викликаючи `Pubkey::find_program_address` з усіма можливими значеннями (0..255). Це працює надійно, але додає обчислювальні витрати під час кожної транзакції.

2. Явний bump через `bump = <значення>`:

#[account(
  seeds = [b"vault", authority.key().as_ref()],
  bump = vault_bump
)]
pub vault: Account<'info, Vault>,

У цьому варіанті bump передається як окремий параметр контексту. Це ефективніше за compute units, але вимагає зберігати bump десь (у даних акаунта або передавати клієнтом).

init — створення PDA з автоматичним seed

Constraint `init` створює новий акаунт за PDA-адресою. Він автоматично:

  1. Обчислює PDA за seeds.
  2. Виділяє місце (за `space`).
  3. Списує rent-exempt депозит із `payer`.
  4. Записує 8-байтовий дискримінатор Anchor.
  5. Встановлює власника на поточну програму.

#[derive(Accounts)]
pub struct CreateVault<'info> {
  #[account(signer, mut)]
  pub authority: Signer<'info>,

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

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

Важливо: при використанні `init` у контексті обов'язково має бути `system_program: Program<'info, System>`. Без цього Anchor не зможе викликати системну інструкцію створення акаунта, і компіляція завершиться помилкою.

Очікуваний результат на Devnet: після виклику інструкції акаунт за адресою PDA існує, має 48 байтів, містить 8-байтовий дискримінатор і належить вашій програмі. Перевірити можна через:

solana account <PDA_ADDRESS> --url devnet

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

ConstraintViolation: як знайти порушений constraint

Коли транзакція падає з `ConstraintViolation`, Anchor додає до логів конкретний підкод. У CLI це виглядає так:

Error: ConstraintViolation: ConstraintSeeds

Ось таблиця найчастіших підкодів та їхніх причин:

Підкод Порушений constraint Ймовірна причина
ConstraintSigner signer Акаунт не підписав транзакцію
ConstraintMut mut Акаунт передано як read-only у транзакції
ConstraintOwner owner Акаунт належить іншій програмі
ConstraintSeeds seeds + bump Переданий ключ не збігається з обчисленим PDA
ConstraintConstraintViolation constraint = ... Вираз у `constraint` повернув `false`
ConstraintSpace space Недостатньо місця для десеріалізації
ConstraintState внутрішній стан акаунта Акаунт не ініціалізовано (немає дискримінатора)

Як діагностувати: у логах транзакції на Devnet шукайте рядок з `Instruction Error` і наступний `Custom` або `ConstraintViolation`. Порівняйте підкод із таблицею вище — це вказує на конкретний акаунт і конкретний constraint.

Невірний bump seed

Це окремий і поширений випадок помилки `ConstraintSeeds`. Виникає в двох ситуаціях:

1. Клієнт передає неправильний bump. Якщо ви використовуєте явний `bump = vault_bump`, клієнтська частина має передати той самий bump, який був знайдений при створенні PDA. Якщо bump зберігався в даних акаунта — клієнт має його прочитати перед формуванням транзакції.

2. Зміна seeds після створення. Якщо при `init` ви використали `seeds = [b"vault", authority.key().as_ref()]`, а при наступному виклику передали `seeds = [b"Vault", authority.key().as_ref()]` (різниця в регістрі) — PDA буде іншим, і перевірка впаде.

Безпечний підхід для початківців: завжди використовуйте автоматичний bump (без явного значення) в `seeds + bump`. Це повільніше за compute units, але виключає клас помилок із невірним bump. Оптимізувати можна пізніше, коли програма працює стабільно.

Перевірка на Devnet: щоб переконатися, що bump правильний, обчисліть PDA локально і порівняйте з акаунтом у транзакції:

solana resolve-tokens --url devnet <ADDRESS>

Або використовуйте `anchor keys list` у проєкті — він виведе всі PDA, визначені в `Anchor.toml`.

Наступний крок у вивченні Anchor — зрозуміти, як фреймворк описує інтерфейс вашої програми для клієнтів. Про це — у матеріалі Що таке IDL у Anchor.

Джерела