Перевантаження облікового запису (account overflow) — це ситуація, коли програма намагається записати в акаунт більше даних, ніж виділено під його буфер. На Solana це не просто помилка серіалізації: залежно від того, як саме відбувається запис, наслідки варіюються від відмови в обслуговуванні до тихого пошкодження даних. Нижче — покрокова методика перевірки програми на цей вектор, з конкретними командами, очікуваними результатами та межами застосування кожного підходу.
Ризики перевантаження: data corruption, denial of service
На Solana існують три фактичні сценарії перевантаження, кожен із власним механізмом пошкодження:
- Панік часу виконання через вихід за межі буфера. Якщо програма отримує мутабельне посилання на дані акаунта через
try_borrow_mut_data()і записує за індексом, що перевищуєdata.len(), runtime Solana генерує паніку. Транзакція відхиляється атомарно — дані не пошкоджуються, але операція, яка залежить від цього оновлення, стає недоступною. Це класичний denial of service для користувачів, які не можуть виконати критичну дію (наприклад, закрити позицію або вивести кошти). - Логічне пошкодження даних (data corruption) при ручному керуванні зміщеннями. Якщо розробник пакує кілька структур у буфер одного акаунта й обчислює зміщення (offset) вручну, зростання однієї структури може перезаписати дані іншої. Runtime не виявляє цього, оскільки запис відбувається в межах загального буфера. Наприклад, якщо конфігураційна секція акаунта зростає з 64 до 80 байтів, а наступна секція починається зі зміщенням 64, перші 16 байтів наступної секції будуть затерті без жодного повідомлення про помилку.
- Відмова серіалізації через перевищення розміру. Anchor та borsh перевіряють розмір цільового буфера перед записом. Якщо серіалізовані дані перевищують
data_len(), транзакція повертає помилкуAccountDataTooSmall. Це безпечний сценарій у плані цілісності даних, але він створює denial of service, якщо акаунт досяг своєї межі, а механізм розширення не реалізований.
Ключове розрізнення для аудитора: перший і третій сценарії виявляються автоматично (транзакція падає), тоді як другий — проходить непомітно й потребує цілеспрямованої перевірки.
Перевірка лімітів розміру акаунтів
Перевірка складається з двох етапів: статичного аналізу структури даних і динамічного тестування на межових значеннях.
Статичний аналіз розміру структур
Для кожного облікового запису, який програма модифікує, визначте максимальний розмір серіалізованих даних. У Anchor це можна зробити за допомогою трейту AnchorSerialize:
assert!(std::mem::size_of::<YourAccountStruct>() <= allocated_space);
Однак size_of повертає розмір структури в пам'яті Rust, який може відрізнятися від серіалізованого представлення через вирівнювання (alignment). Надійніший підхід — серіалізувати об'єкт із максимальними значеннями всіх полів і виміряти фактичний розмір:
let max_instance = YourAccountStruct { field: MAX_VALUE, .. };
let serialized = max_instance.try_to_vec().unwrap();
println!("max serialized size: {}", serialized.len());
Порівняйте це значення з простором, виділеним при створенні акаунта. Якщо використовується init у Anchor, перевірте параметр space у макросі #[account]. Зверніть увагу: Anchor автоматично додає 8 байтів на дискримінатор, тому виділений простір має бути не меншим за serialized.len() + 8.
Динамічне тестування на межових значеннях
Створіть тест, який послідовно заповнює всі змінні поля акаунта максимальними значеннями і викликає кожну інструкцію, що модифікує цей акаунт.
Середовище: solana-test-validator або solana-program-test.
Передумови: розгорнута програма, створений тестовий акаунт із мінімально допустимим простором.
Очікуваний результат: транзакція або успішно виконується (якщо простір достатній), або повертає очікувану помилку (якщо перевірка реалізована), але не панікує.
Кроки:
- Створіть акаунт із простором, рівним мінімально необхідному для початкового стану (без запасу на зростання).
- Викличте інструкцію, яка збільшує дані (додає елемент у Vec, збільшує лічильник, дописує поле).
- Зафіксуйте результат: помилка Anchor, паніка runtime чи тихе пошкодження.
- Повторіть для кожної інструкції, що модифікує акаунт.
Якщо на кроці 2 ви отримуєте паніку замість керованої помилки — програма вразлива до DoS без зрозумілого повідомлення для користувача. Якщо на кроці 2 транзакція успішна, але дані іншої секції акаунта змінилися — це data corruption, і його треба шукати через порівняння дампів буфера до й після виклику.
Перевірка ручних зміщень
Якщо в програмі є код, що обчислює зміщення вручну (наприклад, &mut data[offset..offset + size]), перевірте кожне таке місце:
- Чи залежить
offsetвід даних, які можуть змінюватися під час виконання? - Чи є перевірка
offset + size <= data.len()перед доступом до слайсу? - Чи може зростання однієї секції змістити початок іншої?
Для кожного знайденого випадку складіть тест, який максимізує розмір попередніх секцій і перевіряє, що наступна секція читається коректно.
Патерни безпечного розширення даних
Якщо перевірка виявила, що акаунт може досягти межі, є два перевірені патерни розширення. Вибір між ними залежить від версії runtime Solana та архітектурних обмежень.
Інструкція realloc (in-place розширення)
Починаючи з версії Solana runtime 1.14.0, доступна системна інструкція realloc, яка дозволяє збільшити розмір даних акаунта на місці без міграції. У Anchor цей механізм інтегрований через атрибут #[account(mut, realloc = ...)].
Передумови: Solana runtime 1.14.0 або новіша (перевірте версію кластера, на який деплоїте). Платник (payer) має достатньо SOL для покриття додаткового rent-exempt резерву.
Ризики: якщо платник не має достатньо SOL, транзакція відхиляється. Якщо realloc викликається всередині CPI-виклику, переконайтеся, що зовнішня програма передала достатньо lamports для покриття різниці.
Відкат: якщо транзакція з realloc падає на пізнішому етапі, розширення відкочується атомарно разом із усіма іншими змінами в транзакції.
Обов'язкова перевірка при використанні realloc: переконайтеся, що новий розмір не перевищує 10 МБ (поточна максимальна межа розміру даних акаунта на Solana). Додайте явну перевірку:
const MAX_ACCOUNT_SIZE: usize = 10 * 1024 * 1024;
let new_size = current_size + additional_bytes;
require!(new_size <= MAX_ACCOUNT_SIZE, CustomError::AccountTooLarge);
Патерн міграції акаунтів
Для кластерів або середовищ, де realloc недоступний, використовується міграція: створення нового акаунта з більшим простором, копіювання даних, оновлення посилань і закриття старого акаунта.
Кроки реалізації:
- Створіть новий акаунт (new_account) із розширеним простором через
initабо системну інструкціюcreate_account. - Серіалізуйте поточний стан старого акаунта (old_account) і десеріалізуйте його в new_account.
- Оновіть усі посилання на old_account у відповідних структурах програми (наприклад, у батьківському акаунті або в конфігурації).
- Закрийте old_account, повернувши rent платнику.
Ризики: якщо кроки 2 і 3 не виконані атомарно в одній транзакції, існує вікно, коли обидва акаунти існують паралельно, і програма має коректно обробляти обидва варіанти. Якщо посилання оновлені не повністю, частина логіки продовжить працювати зі старим акаунтом.
Відкат: оскільки всі кроки виконуються в одній транзакції, відбувається автоматичний відкат при будь-якій помилці. Проте, якщо міграція реалізована як багатокроковий процес (що є антипатерном), потрібен ручний відкат: перевірка стану, визначення, на якому кроці сталася помилка, і виконання зворотних операцій.
Обмеження: цей патерн не підходить, якщо акаунт має зовнішні посилання з інших програм, які ви не контролюєте — ви не зможете оновити їхні посилання.
Моніторинг використання простору акаунтів
Проактивний моніторинг дозволяє виявити наближення до межі до того, як користувачі зіткнуться з помилками.
Внутрішньопрограмний моніторинг
Додайте логування співвідношення використаного простору до виділеного у кожній інструкції, що модифікує акаунт:
let used = serialized_len;
let allocated = account_info.data_len();
let ratio = used as f64 / allocated as f64;
msg!("Account space usage: {}/{} ({:.1}%)", used, allocated, ratio * 100.0);
Ці логи доступні через RPC-виклик getTransaction з параметром encoding: "json" у полі logMessages. Налаштуйте парсинг логів у вашій інфраструктурі й алертинг при перевищенні порогу (наприклад, 80%).
Зовнішній моніторинг через RPC
Для критичних акаунтів (наприклад, глобальних конфігураційних акаунтів або пулів ліквідності) періодично опитуйте їхній розмір через RPC:
solana account <account_pubkey> --output json</account_pubkey>
У відповіді поле data містить масив даних, а його довжина — це фактичний виділений простір. Порівняйте його з очікуваним максимальним розміром для цього типу акаунта.
Передумови: доступ до RPC-вузла з увімкненим методом getAccountInfo.
Частота опитування: визначається інтенсивністю записів у акаунт. Для акаунтів, що оновлюються кожні кілька хвилин, достатньо перевірки раз на хвилину. Для рідко оновлюваних — раз на годину.
Обмеження: цей метод показує виділений простір, а не фактичний обсяг серіалізованих даних. Щоб отримати фактичне використання, потрібно десеріалізувати дані й виміряти розмір, що вимагає знання формату програми.
Метрики для алертингу
| Метрика | Як обчислити | Поріг алерту |
|---|---|---|
| Відсоток заповнення акаунта | serialized_len / data_len() * 100 |
80% — попередження, 95% — критичний |
Кількість транзакцій з AccountDataTooSmall за годину |
Підрахунок у логах RPC або через on-chain аналітику | Більше 0 — негайне розслідування |
| Кількість панік runtime у транзакціях з конкретним акаунтом | Аналіз полів err у відповідях getTransaction |
Більше 0 — негайне розслідування |
Якщо алерт спрацював на етапі 80% заповнення, у вас є час на планове розширення акаунта через realloc або міграцію до того, як користувачі почнуть отримувати помилки. Якщо алерт спрацював на рівні помилок — це вже інцидент, і подальші дії належать до процедури incident response.
Перевірка на перевантаження облікових записів не є разовою дією: вона має бути інтегрована в CI-пipeline як окремий тестовий набір і доповнена runtime-моніторингом для вже деплойованих програм. Тільки комбінація статичного аналізу, граничного тестування та спостереження за production-середовищем дає впевненість, що акаунти не досягнуть межі непомітно.