Signer у Solana — це акаунт, чий приватний ключ використано для створення криптографічного підпису транзакції. Саме цей підпис доводить рантайму Solana, що власник акаунта дійсно авторизував виконання інструкції. Без правильного розуміння signers неможливо написати жодну безпечну програму на Solana.
Означення signer
Акаунт, що підписує транзакцію криптографічним ключем
Кожна транзакція в Solana містить набір інструкцій. Кожна інструкція посилається на список акаунтів, і для кожного акаунта вказується прапорець is_signer (true або false). Рантайм перевіряє: якщо для акаунта вказано is_signer: true, у транзакції має бути валідний Ed25519-підпис, створений відповідним приватним ключем. Якщо підпис відсутній чи недійсний — транзакція відхиляється до того, як будь-яка програма отримає управління.
На рівні серіалізації це виглядає так: передані акаунти описуються структурою AccountMeta, де окремо вказується is_signer і is_writable. Це два незалежні прапорці.
Чому signer не обов'язково є writable
Поширена помилка — вважати, що signer завжди змінює свій стан. Насправді is_signer і is_writable — ортогональні властивості. Акаунт може бути signer, але не writable, і навпаки.
Приклад: ви викликаєте інструкцію, яка читає дані з вашого акаунта, але не змінює їх. Програмі потрібна ваша авторизація (signer), але записувати у ваш акаунт нічого не треба. Інший приклад — системна програма Transfer: акаунт відправника є одночасно signer і writable (зменшується баланс), а акаунт одержувача — лише writable.
| is_signer | is_writable | Типовий сценарій |
|---|---|---|
| true | true | Відправник SOL, ініціатор зміни стану |
| true | false | Авторизація без зміни власного акаунта |
| false | true | Акаунт-ціль, PDA, що змінюється програмою |
| false | false | Акаунт лише для читання (конфігурація, oracle) |
Роль signers у безпеці
Авторизація дій: витрати, делегування, зміна стану
У Solana немає централізованого «власника» даних у традиційному розумінні. Натомість авторизація будується на тому, хто підписав транзакцію. Програма сама визначає логіку: який саме signer має право виконати цю інструкцію і над якими акаунтами.
Типові випадки, де signer є обов'язковим:
- Витрати токенів або SOL — програма перевіряє, що signer є власником акаунта, з якого списуються кошти.
- Делегування — ви підписуєте інструкцію Approve у Token Program, дозволяючи іншому акаунту витрачати токени від вашого імені.
- Ініціалізація PDA — signer сплачує комісію за створення акаунта (rent) і стає його логічним власником.
- Закриття акаунта — signer підтверджує, що згоден повернути rent на свій акаунт.
Що відбувається без підпису signer
Якщо програма очікує signer, а прапорець is_signer не встановлено або підпис невалідний, рантайм Solana відхиляє транзакцію з помилкою ще до виклику самої програми. Це означає, що програма-зловмисник не може обійти перевірку signer на рівні логіки — рантайм гарантує, що якщо програма бачить is_signer: true, підпис вже перевірено криптографічно.
Однак є нюанс: якщо програма не позначила акаунт як signer у структурі Accounts, але намагається перевірити підпис вручну — це антипатерн. Рантайм не передасть підпис у програму для акаунтів без прапорця is_signer. Правильний шлях — завжди покладатися на механізм is_signer рантайму.
Signer у контексті програми
Як програма перевіряє signer через Anchor constraints
У фреймворку Anchor signer позначається обмеженням signer у структурі контексту акаунтів. Anchor автоматично додає перевірку: якщо акаунт позначений як signer, але рантайм не підтвердив підпис — інструкція завершується помилкою MissingRequiredSignature.
Середовище для прикладу нижче:
- Anchor 0.30.1
- Solana CLI 1.18.26
- Rust 1.75.0+
- Кластер: Devnet
Концептуальний приклад — інструкція, яка створює акаунт даних і записує публічний ключ signer як власника:
Цей приклад призначений для навчання на Devnet. Для production використовуйте додаткові перевірки (seeds, has_one, constraints), які розглядаються в наступних розділах.
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod signer_demo {
use super::*;
pub fn create_data(ctx: Context<CreateData>) -> Result<()> {
ctx.accounts.data_account.owner = ctx.accounts.authority.key();
Ok(())
}
}
#[derive(Accounts)]
pub struct CreateData<'info> {
#[account(mut, signer)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = 8 + 32
)]
pub data_account: Account<'info, DataAccount>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct DataAccount {
pub owner: Pubkey,
}
Тип Signer<'info> у Anchor — це спеціальний обгортковий тип. Він не завантажує дані акаунта (на відміну від Account<'info, T>), а лише надає доступ до key() та гарантує, що акаунт підписав транзакцію.
Приклад: mut та signer разом
Часто один акаунт є одночасно signer і writable. У наведеному вище прикладі authority позначений як #[account(mut, signer)]. Це означає:
signer— рантайм перевірив криптографічний підпис; Anchor додатково перевірить наявність підпису.mut— програма має дозвіл змінювати дані цього акаунта (у цьому випадку — списувати lamports для оплати rent нового акаунта через системну програму).
Якщо прибрати signer, але залишити mut, Anchor дозволить компіляцію, але під час виконання системна програма відхилить інструкцію, бо акаунт, що сплачує rent, не підписав транзакцію.
Якщо прибрати mut, але залишити signer, компіляція також успішна, але під час виконання виникне помилка, бо init потребує списання lamports з payer, а payer не позначений як writable.
Типові помилки
Signature verification failed
Помилка Signature verification failed генерується рантаймом Solana, коли прапорець is_signer встановлено для акаунта, але наданий підпис не відповідає приватному ключу цього акаунта. Це може статися, якщо:
- Ви передали неправильний ключову пару при підписанні (наприклад, замість ключа акаунта-відправника використали ключ іншого акаунта).
- Підпис було створено для іншої транзакції (повторне використання підпису неможливе — кожен підпис прив'язаний до конкретного хешу транзакції).
- Підпис пошкоджено під час серіалізації або передачі.
Спосіб перевірки на Devnet: сформуйте транзакцію через solana-cli з прапорцем --verbose і переконайтеся, що pubkey у списку signers збігається з pubkey акаунта, якому ви передаєте is_signer: true.
Missing required signer
Помилка Missing required signer — це Anchor-специфічна помилка (код 6000). Вона виникає, коли в структурі Accounts акаунт позначено обмеженням signer, але рантайм не отримав валідного підпису для цього акаунта. Тобто is_signer у транзакції встановлено в false, тоді як Anchor очікує true.
Типова причина — клієнтський код не додав акаунт до масиву signers при побудові транзакції. Наприклад, у TypeScript-клієнті на Anchor:
// Неправильно — authority не додано до signers
await program.methods
.createData()
.accounts({
authority: wallet.publicKey,
dataAccount: dataAccountPda,
systemProgram: SystemProgram.programId,
})
.rpc();
// Правильно — wallet.payer передає підпис
await program.methods
.createData()
.accounts({
authority: wallet.publicKey,
dataAccount: dataAccountPda,
systemProgram: SystemProgram.programId,
})
.signers([wallet.payer])
.rpc();
У більшості випадків, якщо ви використовуєте AnchorProvider з wallet-адаптером, .rpc() автоматично додає wallet як signer. Але якщо ви працюєте з кількома ключовими парами або PDA-signers через invoke_signed, потрібно явно вказувати всі signers.
Наступний крок
Тепер, коли ви розумієте, як signers працюють на рівні рантайму та програми, логічний наступний крок — зрозуміти, скільки коштує виконання транзакцій із signers. Перейдіть до матеріалу Комісії транзакцій у Solana для початківця, де детально розібрано, як signers впливають на розмір транзакції та вартість комісії.