Стиснення даних в облікових записах Solana — це інженерне рішення, яке безпосередньо впливає на вартість депозиту (rent-exempt minimum), розмір транзакції та обчислювальні витрати. Нижче наведено перевірені підходи до стиснення, їхні обмеження та покрокову процедуру міграції існуючих даних.
Методи стиснення: borsh та custom encoding
Borsh: стандартне серіалізаційне форматування
Borsh (Binary Object Representation Serializer for Hashing) — це типовий для Solana формат фіксованої схеми. Він детермінований, компактніший за JSON, але не є алгоритмом стиснення у класичному розумінні. Borsh усуває накладні витрати на ключі та роздільники, проте зберігає дані у початковому розмірі щодо типів.
Коли borsh достатній:
- Структура даних містить переважно фіксовані типи (u8, u16, u32, u64, i64, bool, Pubkey).
- Масиви мають відому або обмежену довжину.
- Розмір облікового запису вже близький до мінімально необхідного.
Обмеження borsh:
- Рядки зберігаються як u32-довжина + байти UTF-8 без внутрішнього стиснення.
- Enum-типи займають місце відповідно до розміру найбільшого варіанту плюс 1 байт для тегу.
- Option<T> завжди додає 1 байт на дискримінатор навіть для None.
Custom encoding: бітове пакування та домен-специфічне стиснення
Custom encoding передбачає ручне перетворення даних у компактніше бітове подання перед записом у обліковий запис. Це не стандартна серіалізація, а спеціалізована логіка пакування/розпакування.
Типові техніки:
- Бітове пакування (bit packing). Зберігання булевих прапорців або переліків із малою кількістю варіантів не по 1 байту на значення, а щільно у бітах. Наприклад, 8 булевих полів — це 1 байт замість 8.
- Змінна довжина для цілих чисел (varint). Замість фіксованого u64 (8 байт) для значень, що рідко перевищують 255, використовується 1–9 байтів залежно від величини.
- Словники та інтервальне кодування. Для повторюваних рядків або переліків замість повного збереження зберігається індекс у словнику, розміщений окремо або вбудований у структуру.
- Delta-кодування для часових рядків. Зберігається різниця між послідовними значеннями, а не абсолютні величини.
Коли custom encoding виправданий:
- Обліковий запис наближається до максимального розміру (10 KB) або перевищує економічно доцільний поріг rent-exempt депозиту.
- Дані мають високу рівень надлишковості (повторювані рядки, багато прапорців, щільні переліки).
- Збереження кожного кілобайта дає помітну економію SOL при масштабуванні тисяч або мільйонів облікових записів.
Трейдофи між стисненням та compute cost
Стиснення даних не є безкоштовним. Кожна операція пакування та розпакування споживає compute units (CU). Баланс між економією місця та обчислювальними витратами — це ключове інженерне рішення.
Фактори, що визначають чисту економію:
- Частота читання vs запису. Якщо обліковий запис читається часто (наприклад, через кожну транзакцію користувача), а переписується рідко, витрати на розпакування при кожному читанні швидко перевищать економію на rent. Навпаки, для даних, що записуються один раз і читаються рідко, стиснення майже завжди вигідне.
- Складність алгоритму. Просте бітове пакування коштує десятки CU. Varint-декодування — сотні. Спроба використати щось на кшталт LZ77 або Huffman у смарт-контракті зажере значну частину бюджету (зараз 200 000 CU за замовчуванням, 1 400 000 з підвищеним лімітом).
- Розмір економії. Економія 50 байт на обліковому записі при вартості розпакування 5 000 CU на кожне читання — це збиткова операція для високочастотних сценаріїв.
Практична евристика:
- Не стискайте дані, якщо економія менша за 10–15% від початкового розміру облікового запису.
- Не використовуйте custom encoding для даних, що десеріалізуються більше ніж 5–10 разів на один запис.
- Вимірюйте фактичні CU до та після впровадження стиснення за допомогою інструментів профілювання (solana-test-validator з логуванням CU, Anchor logs з unit-тестів).
Межа застосування: у жодному разі не використовуйте загальні алгоритми стиснення (gzip, zstd тощо) всередині смарт-контрактів Solana. Їхня обчислювальна вартість робить це непрактичним у межах поточних compute budget.
Вибір оптимального формату для типу даних
Різні типи даних вимагають різних стратегій. Нижче наведено матрицю рішень на основі фактичних характеристик даних.
| Тип даних | Рекомендований підхід | Обґрунтування |
|---|---|---|
| Фіксовані числові поля (баланси, лічильники, таймстемпи) | Borsh без змін | Фіксований розмір вже оптимальний. Varint додає витрати на декодування без значної економії для u64. |
| Булеві прапорці (3–20 штук) | Бітове пакування | Економія від 2 до 19 байт. Декодування — одна бітова маска, мінімальні CU. |
| Enum з 2–4 варіантами | Custom: 2 біти замість 1 байта | Економія мала, але декодування тривіальне. Виправдано лише при великій кількості таких полів. |
| Короткі рядки (до 32 байт), що повторюються | Словникове індексування | Замість 32 байт — 1–2 байти індексу. Словник розміщується в окремому обліковому записі. |
| Довгі рядки (URI, метадані) | Зовнішнє зберігання (Arweave, IPFS) + хеш у обліковому записі | Не стиснення, а архітектурне рішення. 32-байтний хеш замість кілобайтів тексту. |
| Масиви фіксованих структур з відомими межами значень | Custom: усікання до мінімально необхідної кількості бітів на поле | Наприклад, якщо значення поля завжди 0–1000, достатньо 10 бітів замість 16 (u16). |
| Часові рядки (ціни, баланси за період) | Delta-кодування + varint | Різниці між сусідніми значеннями зазвичай малі, varint кодує їх компактно. |
Критерій прийняття рішення: обчисліть рентабельність стиснення — відношення економії у байтах до додаткових CU на операцію. Якщо один збережений кілобайт коштує менше ніж 500 додаткових CU на типове читання — стиснення виправдане. Ці порогові значення потребують перевірки в контексті конкретного застосунку та поточних тарифів на rent.
Міграція існуючих даних до нового формату
Міграція — це найскладніший етап, оскільки вимагає збереження консистентності даних під час переходу. Нижче — перевірена процедура.
Передумови:
- Розгорнута програма з новою логікою серіалізації/десеріалізації.
- Написані та пройдені unit-тести для двосторонньої конвертації (старий формат → новий → старий) з перевіркою рівності даних.
- Підготовлена інструкція для індексерів та офчейн-сервісів, що читають ці облікові записи.
Покрокова процедура:
- Додайте версіонування облікового запису. Перший байт облікового запису має бути дискримінатором версії (наприклад, 0x00 — старий формат, 0x01 — новий). Це дозволяє програмі коректно обробляти обидва формати під час перехідного періоду.
- Реалізуйте інструкцію міграції. Інструкція повинна: читати дані у старому форматі, конвертувати у новий, перевіряти інваріанти (розмір не перевищує новий ліміт, дані не втрачено), записувати новий формат, оновлювати версійний дискримінатор.
- Зменште розмір облікового запису (за потреби). Якщо новий формат менший, викличте
reallocз прапоромRealloc::zeroдля звільнення місця та повернення зайвого rent депозиту. У Anchor це доступно через атрибут#[account(realloc = ...)]. - Запустіть міграцію на testnet/devnet. Прогнайте міграцію на копії реальних даних. Перевірте: коректність конвертації, фактичні CU інструкції міграції, поведінку індексерів.
- Виконайте міграцію на mainnet-beta. Міграцію можна проводити пакетно (batch migration) через окремі транзакції для кожного облікового запису або через спеціалізований скрипт із Cron-like логікою.
- Видаліть підтримку старого формату. Після підтвердження повної міграції видаліть код обробки версії 0x00. Це зменшить розмір програми та усуне непотрібні гілки виконання.
Очікуваний результат: облікові записи мігровані до нового формату, rent-deposit зменшено відповідно до нового розміру, програма обробляє лише новий формат.
Ризики:
- Неповна міграція. Якщо частина облікових записів не мігрована, а підтримку старого формату вже видалено — ці облікові записи стають недоступні. Запобігання: додавайте моніторинг кількості облікових записів кожної версії.
- Помилка конвертації з втратою даних. Запобігання: обов'язкові інваріанти в інструкції міграції (перевірка контрольних сум, порівняння розпакованих даних з оригіналом перед записом).
- Перевищення compute budget інструкції міграції. Для великих облікових записів конвертація може бути дорогою. Запобігання: профілювання CU на testnet, розбиття міграції на кілька інструкцій за потреби.
План відкату:
- Зберігайте стару версію програми (upgrade authority не віддана, бекап deploy-пакету збережено).
- Інструкція міграції повинна підтримувати зворотну конвертацію (новий формат → старий) або мати можливість відновити версійний дискримінатор без зміни даних.
- У разі критичної помилки виконайте downgrade програми до попередньої версії та повторіть міграцію з виправленням.
Перевірка після міграції:
- Прочитайте кілька мігрованих облікових записів через RPC і переконайтеся, що десеріалізація у новому форматі повертає коректні дані.
- Перевірте, що rent-exempt статус збережено (баланс облікового запису не менший за новий мінімум).
- Перевірте логи індексерів та офчейн-сервісів на відсутність помилок парсингу.
Наступний логічний крок: після оптимізації розміру облікових записів варто перевірити, як стиснення впливає на взаємодію з іншими програмами через CPI — про це детально у матеріалі про роботу з CPI та його вплив на compute budget.