Стиснення даних в облікових записах 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-тести для двосторонньої конвертації (старий формат → новий → старий) з перевіркою рівності даних.
  • Підготовлена інструкція для індексерів та офчейн-сервісів, що читають ці облікові записи.

Покрокова процедура:

  1. Додайте версіонування облікового запису. Перший байт облікового запису має бути дискримінатором версії (наприклад, 0x00 — старий формат, 0x01 — новий). Це дозволяє програмі коректно обробляти обидва формати під час перехідного періоду.
  2. Реалізуйте інструкцію міграції. Інструкція повинна: читати дані у старому форматі, конвертувати у новий, перевіряти інваріанти (розмір не перевищує новий ліміт, дані не втрачено), записувати новий формат, оновлювати версійний дискримінатор.
  3. Зменште розмір облікового запису (за потреби). Якщо новий формат менший, викличте realloc з прапором Realloc::zero для звільнення місця та повернення зайвого rent депозиту. У Anchor це доступно через атрибут #[account(realloc = ...)].
  4. Запустіть міграцію на testnet/devnet. Прогнайте міграцію на копії реальних даних. Перевірте: коректність конвертації, фактичні CU інструкції міграції, поведінку індексерів.
  5. Виконайте міграцію на mainnet-beta. Міграцію можна проводити пакетно (batch migration) через окремі транзакції для кожного облікового запису або через спеціалізований скрипт із Cron-like логікою.
  6. Видаліть підтримку старого формату. Після підтвердження повної міграції видаліть код обробки версії 0x00. Це зменшить розмір програми та усуне непотрібні гілки виконання.

Очікуваний результат: облікові записи мігровані до нового формату, rent-deposit зменшено відповідно до нового розміру, програма обробляє лише новий формат.

Ризики:

  • Неповна міграція. Якщо частина облікових записів не мігрована, а підтримку старого формату вже видалено — ці облікові записи стають недоступні. Запобігання: додавайте моніторинг кількості облікових записів кожної версії.
  • Помилка конвертації з втратою даних. Запобігання: обов'язкові інваріанти в інструкції міграції (перевірка контрольних сум, порівняння розпакованих даних з оригіналом перед записом).
  • Перевищення compute budget інструкції міграції. Для великих облікових записів конвертація може бути дорогою. Запобігання: профілювання CU на testnet, розбиття міграції на кілька інструкцій за потреби.

План відкату:

  • Зберігайте стару версію програми (upgrade authority не віддана, бекап deploy-пакету збережено).
  • Інструкція міграції повинна підтримувати зворотну конвертацію (новий формат → старий) або мати можливість відновити версійний дискримінатор без зміни даних.
  • У разі критичної помилки виконайте downgrade програми до попередньої версії та повторіть міграцію з виправленням.

Перевірка після міграції:

  • Прочитайте кілька мігрованих облікових записів через RPC і переконайтеся, що десеріалізація у новому форматі повертає коректні дані.
  • Перевірте, що rent-exempt статус збережено (баланс облікового запису не менший за новий мінімум).
  • Перевірте логи індексерів та офчейн-сервісів на відсутність помилок парсингу.

Наступний логічний крок: після оптимізації розміру облікових записів варто перевірити, як стиснення впливає на взаємодію з іншими програмами через CPI — про це детально у матеріалі про роботу з CPI та його вплив на compute budget.

Джерела