Версії для прикладів у «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-адресою. Він автоматично:
- Обчислює PDA за seeds.
- Виділяє місце (за `space`).
- Списує rent-exempt депозит із `payer`.
- Записує 8-байтовий дискримінатор Anchor.
- Встановлює власника на поточну програму.
#[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.