Транзакційні патерни на Solana — це не абстракції ради абстракцій. Це конкретні інженерні рішення, які визначають, чи ваш застосунок оброблятиме сотні операцій на секунду, чи застрягне на десятках через конфлікти блокування та перевищення лімітів. Нижче розглянуто чотири патерни, які безпосередньо впливають на пропускну здатність: пакетна обробка, паралелізм без конфліктів, lookup table для зменшення розміру та обмеження, що їх накладає slot time.

Пакетна обробка транзакцій

Пакетна обробка (batching) — це відправка групи логічно пов'язаних транзакцій як єдиної одиниці роботи клієнтської сторони. На рівні рантайму Solana кожна транзакція все одно обробляється окремо, але пакетний підхід дає контроль над порядком, обробкою помилок та відновленням.

Коли пакетна обробка має сенс

  • Масове зарахування токенів (airdrop, розподіл винагород).
  • Ініціалізація кількох облікових записів (accounts) перед основною логікою.
  • Багатокрокові міграції стану, де кожен крок — окрема транзакція.

Механіка реалізації

Ключовий принцип — підготовка всіх транзакцій заздалегідь, їх підписання та відправка з контролем послідовності. Порядок відправки визначає порядок обробки: якщо транзакція B залежить від стану, який створює транзакція A, A має бути відправлена та підтверджена першою.

На практиці це означає:

  1. Зібрати масив інструкцій, розбитих на транзакції з урахуванням лімітів розміру (1232 байти) та обчислювального бюджету (compute budget).
  2. Підписати кожну транзакцію. Якщо використовується Keystone або інший засіб зовнішнього підписання, підписи треба отримати до відправки будь-якої транзакції з пакета.
  3. Відправляти транзакції послідовно, перевіряючи підтвердження кожної перед відправкою наступної (для залежних операцій), або паралельно (для незалежних).

Обробка часткових невдач

У пакеті з N транзакцій може бути підтверджено K < N. Типова помилка — вважати, що невдача однієї транзакції скасовує весь пакет. Рантайм Solana так не працює: кожна транзакція оцінюється незалежно. Тому клієнтська логіка зобов'язана:

  • Вести мапу signature → intent для кожної транзакції в пакеті.
  • Перевіряти статус кожного signature окремо через getSignatureStatuses.
  • Для транзакцій, що повернули помилку на рівні префікс-перевірок (preflight), аналізувати конкретний код помилки, а не припущення про причину.
  • Реалізувати повторну відправку лише для тих транзакцій, які не потрапили в блок через ліміт блоку, а не для тих, що мають логічну помилку (наприклад, insufficient funds).

Межі застосування

Пакетна обробка не підходить для операцій, де користувач очікує атомарність: якщо треба переказати токени та одночасно оновити стан off-chain сервісу, а друга частина залежить від першої, пакет без додаткового механізму узгодження ( reconciliation ) дає вікно для часткового стану.

Паралельні транзакції без конфліктів акаунтів

Solana використовує модель блокування на рівні облікових записів (account-level locking). Під час обробки транзакції рантайм накладає блокування на всі акаунти, зазначені як writable. Дві транзакції в одному слоті не можуть одночасно записувати в один і той самий акаунт. Це фундаментальне обмеження, і єдиний спосіб масштабуватися в його межах — уникати конфліктів на етапі архітектури.

Модель блокування

  • Читання (readonly): кілька транзакцій можуть одночасно читати один акаунт.
  • Запис (writable): лише одна транзакція в межах одного слота може мати writable доступ до конкретного акаунта.

Транзакція, що конфліктує за writable-акаунт із вже заблокованою транзакцією в поточному слоті, переноситься на наступний слот. Це не є помилкою, але безпосередньо знижує ефективну пропускну здатність.

Патерн фанкціонування стану (state fan-out)

Замість одного центрального акаунта, у який записують усі операції, стан розбивається на сегменти за ключем партиціювання. Наприклад, замість єдиного акаунта лідерборду з 10 000 записів створюється 100 акаунтів по 100 записів. Кожен сегмент має власний PDA (Program Derived Address), і запис йде лише в той сегмент, який відповідає ключу партиціювання.

Критерій вибору ключа партиціювання — рівномірність розподілу записів. Якщо використовується ідентифікатор користувача як ключ, а 80% операцій належать 5% користувачів, конфлікти не зникнуть. Хешування або рівномірне розподілення за діапазонами дає кращий результат.

Патерн розділення читання та запису

Якщо акаунт містить конфігурацію, яку часто читають, але рідко оновлюють, винесіть конфігурацію в окремий readonly-акаунт для транзакцій, що не потребують запису. Це дозволяє рантайму не блокувати конфігураційний акаунт для кожної транзакції читання.

Діагностика конфліктів

Якщо ви спостерігаєте, що транзакції підтверджуються, але з затримкою в кілька слотів без очевидної причини, перевірте:

  1. Чи є спільний writable-акаунт у транзакціях, що відправляються паралельно.
  2. Чи не блокує цей акаунт інша транзакція з високим compute budget, що займає кілька слотів.
  3. Чи не використовується один і той самий PDA-акаунт як буфер для тимчасових даних у різних транзакціях.

Патерн lookup table для зменшення розміру

Кожна транзакція на Solana обмежена 1232 байтами. Кожен адрес акаунта в транзакції займає 32 байти. Для транзакцій, що звертаються до великої кількості акаунтів (наприклад, маршрутизація через кілька пулів, масові затвердження, складні CPI-ланцюги), адреси можуть займати значну частину цього ліміту.

Address Lookup Table (ALT) — це механізм, що дозволяє замінити повні 32-байтові адреси в транзакції на 1-байтові індекси, які вказують на адреси, збережені в спеціальному обліковому записі lookup table.

Коли lookup table економно виправданий

  • Транзакція містить понад 20–25 унікальних адрес (точка, де економія байтів стає відчутною).
  • Транзакція наближається до ліміту 1232 байтів без lookup table.
  • Операція виконується повторно (lookup table створюється один раз, використовується багаторазово).

Для транзакцій із 3–5 акаунтами використання lookup table не дає переваг, але додає складність: lookup table треба створити, наповнити адресами та дочекатися активації.

Життєвий цикл lookup table

  1. Створення: виклик інструкції create_lookup_table, яка визначає адресу таблиці та слот, з якого вона стає активною.
  2. Наповнення: виклик extend_lookup_table для додавання адрес. Існує ліміт на кількість адрес у одній таблиці (256).
  3. Активація: таблиця стає доступною для використання в транзакціях лише після настання слота, зазначеного при створенні. До цього моменту транзакції з посиланнями на неї будуть відхилені.
  4. Деактивація та закриття: deactivate_lookup_table та close_lookup_table для звільнення ресурсів, коли таблиця більше не потрібна.

Практичні обмеження

Lookup table — це окремий акаунт, і його читання в транзакції теж коштує compute units. Для кожного адреси, що читається з таблиці, рантайм виконує додаткову роботу. У транзакціях із високим навантаженням на compute budget це треба враховувати: економія байтів не повинна призводити до перевищення compute ліміту.

Також варто пам'ятати, що адреси в lookup table є незмінними після додавання. Якщо контракт оновлюється і змінюється набір акаунтів, потрібна нова таблиця або розширення існуючої (якщо в ній є вільні слоти).

Обмеження масштабування через slot time

Slot time на Solana становить приблизно 400 мілісекунд. Це інтервал, за який лідер має сформувати блок, розповсюдити його та перейти до наступного слота. Цей параметр є архітектурною константою мережі, і його не можна змінити на рівні застосунку.

Що slot time означає для пропускної здатності

Навіть якщо ваші транзакції ідеально розпаралелені, не конфліктують за акаунти та мають мінімальний розмір, максимальна кількість слотів фіксована: приблизно 2,5 слота в секунду. Кожен слот має власні обмеження:

  • Максимальний розмір блоку (в байтах).
  • Максимальний обчислювальний бюджет блоку (total compute units per block).
  • Максимальна кількість транзакцій у блоці.

Ці ліміти є верхньою межею для всіх транзакцій у мережі, а не для вашого застосунку зокрема. Ваша ефективна пропускна здатність — це частка цих лімітів, яку ви реально отримуєте.

Як працювати в межах slot time

Оскільки slot time не змінюється, оптимізація зосереджується на тому, щоб кожна транзакція використовувала мінімально необхідний обсяг ресурсів блоку:

  • Мінімізувати розмір транзакції: менше байтів — більше транзакцій поміщається в блоці. Lookup table (див. вище) — один із інструментів.
  • Мінімізувати compute units на транзакцію: оптимізувати логіку програми, уникати зайвих операцій з пам'яттю, використовувати ефективні структури даних.
  • Уникати зайвих конфліктів: кожен конфлікт, що переносить транзакцію на наступний слот, — це втрачений слот для цієї транзакції.

Реалістичні очікування

Теоретична межа мережі відрізняється від практичної доступної пропускної здатності для окремого застосунку. Конкуренція за місце в блоці, змінне навантаження мережі та черги лідерів роблять точне прогнозування неможливим. Замість цього:

  • Вимірюйте фактичну пропускну здатність на devnet з навантаженням, наближеним до реального.
  • Проєктуйте архітектуру так, щоб часткове зниження пропускної здатності не призводило до логічних помилок (наприклад, черги з таймаутами замість жорстких очікувань).
  • Моніторьте метрики підтвердження транзакцій у production та реагуйте на деградацію, а не розраховуйте на постійну максимальну швидкість.

План відкату при досягненні меж

Якщо ваш застосунок системно стикається з обмеженнями slot time:

  1. Перевірте, чи не створюєте ви зайві конфлікти: іноді зміна порядку акаунтів у транзакції або перерозподіл стану між акаунтами дає значне покращення без зміни архітектури.
  2. Оцініть off-chain компонент: чи можна частину обчислень або агрегації винести за межі on-chain транзакцій.
  3. Розгляньте чергування на клієнтській стороні: замість відправки всіх транзакцій одночасно, використовуйте адаптивну чергу, що регулює швидкість відправки на основі поточного часу підтвердження.

Транзакційні патерни масштабування працюють лише в сукупності. Пакетна обробка без усунення конфліктів акаунтів дає мінімальний ефект. Lookup table без оптимізації compute budget може перенести вузьке місце з розміру на обчислення. Тому починайте з вимірювання: визначте, який саме ліміт (розмір, compute, конфлікти, slot) є вузьким місцем для вашого конкретного навантаження, і застосовуйте відповідний патерн цілеспрямовано.

Джерела