Address Lookup Tables (ALT) дозволяють замінити повні 32-байтові публічні ключі в транзакції на 1-байтові індекси посилань на попередньо створену on-chain таблицю. Це прямо зменшує розмір серіалізованої транзакції та звільняє місце для додаткових інструкцій у межах ліміту 1232 байти. Механізм працює виключно з версійованими транзакціями (v0).

Створення та управління address lookup table

Передумови: встановлений CLI Solana (починаючи з версії, що підтримує versioned transactions), Node.js із пакетом @solana/web3.js версії 1.87 або новішої, доступ до RPC-вузла з увімкненою підтримкою v0 транзакцій, підключений гаманець із достатнім балансом для оренди облікового запису таблиці.

Очікуваний результат: створений on-chain обліковий запис типу Address Lookup Table, готовий до прийняття адрес.

Створення таблиці через CLI:

solana address-lookup-table create
--authority <WALLET_PUBKEY>
--payer <WALLET_PUBKEY>
--recent-slot

Прапорець --recent-slot використовує поточний слот як початковий. Це найпростіший варіант для нової таблиці. CLI поверне адресу створеної таблиці та слот активації.

Програмне створення через @solana/web3.js:

Використовується інструкція AddressLookupTableProgram.createLookupTable. Вона повертає об'єкт із транзакцією та адресою таблиці. Транзакцію потрібно підписати та відправити. Після підтвердження таблиця переходить у стан creating і стає доступною для використання після досягнення вказаного слота активації.

Перевірка стану таблиці:

solana address-lookup-table <LOOKUP_TABLE_ADDRESS>

У відповіді звертайте увагу на поля authority, deactivationSlot та список адрес. Якщо deactivationSlot дорівнює максимальному значенню u64, таблиця активна і не запланована до деактивації.

План відкату: якщо створена таблиця виявиться непотрібною, її можна закрити інструкцією AddressLookupTableProgram.closeLookupTable. Оренда повертається на акаунт authority. Усі транзакції, що посилаються на закриту таблицю, будуть відхилені.

Додавання акаунтів до таблиці

Одноразово додати адреси під час створення неможливо — таблиця спочатку порожня. Адреси додаються окремою інструкцією extend.

Обмеження на одну інструкцію extend: максимум 256 адрес. Максимальна місткість однієї таблиці — також 256 адрес.

Через CLI:

solana address-lookup-table extend <LOOKUP_TABLE_ADDRESS>
--address <PUBKEY_1>
--address <PUBKEY_2>
--payer <WALLET_PUBKEY>

Програмне додавання через AddressLookupTableProgram.extendLookupTable:

Потрібно передати масив публічних ключів (PublicKey[]), адресу таблиці, адресу payer та адресу authority. Кожна інструкція extend збільшує розмір облікового запису таблиці, тому payer сплачує різницю в оренді.

Типова помилка: спроба використати таблицю одразу після відправки extend. Інструкція extend змінює дані таблиці, але ці зміни відображаються в lookup лише після того, як слот транзакції extend стане старшим за попередній слот, використаний у lookup. На практиці це означає, що після підтвердження extend достатньо дочекатися одного слота (приблизно 400 мс), перш ніж будувати транзакцію, що посилається на додані адреси.

Фактична причина помилки «Lookup table not found»: транзакція посилається на адресу, яка ще не додана до таблиці на момент обробки, або слот активації ще не досягнутий. Перевірте слот останнього підтвердженого extend і порівняйте зі слотом, у якому обробляється ваша транзакція.

Обмеження lookup tables

Перш ніж проектувати архітектуру навколо ALT, врахуйте наступні жорсткі обмеження:

  • Місткість однієї таблиці — 256 адрес. Це не налаштовується.
  • Кількість таблиць на транзакцію — максимум 3.
  • Кількість lookup-посилань на одну інструкцію — максимум 64 адреси з усіх таблиць разом.
  • Формат транзакції — ALT працює лише з версійованими транзакціями (v0). Legacy-транзакції не підтримують lookup.
  • Деактивація — після інструкції deactivate таблиця перестає приймати нові адреси, але існуючі lookup-посилання продовжують працювати протягом cooldown-періоду (зараз це 512 слотів). Після закінчення cooldown таблицю можна закрити.
  • Вартість — створення та кожне розширення таблиці вимагають сплати rent-exempt мінімуму за збільшений розмір облікового запису. Ці кошти не повертаються, доки таблицю не закрито.
  • Індекси — у межах однієї транзакції індекси адрес у таблиці починаються з 0. Якщо використовується кілька таблиць, індекси продовжуються послідовно: перша таблиця 0–255, друга 256–511, третя 512–767.

Ці обмеження означають, що ALT не підходить для сценаріїв із тисячами унікальних акаунтів в одній транзакції. Максимальна кількість адрес, яку можна посилатися через lookup в одній транзакції, — 768 (3 таблиці × 256).

Практичний приклад зменшення розміру транзакції

Сценарій: транзакція містить інструкцію, яка читає 20 різних облікових записів (наприклад, пакетне оновлення стану або агреговане CPI). Без ALT кожен акаунт займає 32 байти в масиві account_keys транзакції.

Крок 1. Підготовка середовища.

Переконайтеся, що RPC-вузол підтримує versioned transactions. Визначте 20 адрес акаунтів, які постійно використовуються у ваших транзакціях. Ці адреси мають бути стабільними — ALT не підходить для одноразових акаунтів, оскільки накладні витрати на створення та розширення таблиці не окупляться.

Крок 2. Створення таблиці та додавання адрес.

Створіть таблицю, як описано вище. Додайте всі 20 адрес однією інструкцією extend. Дочекайтеся підтвердження та одного додаткового слота.

Крок 3. Отримання стану таблиці.

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

Крок 4. Побудова версійованої транзакції з lookup.

Замість стандартного TransactionMessage використовується TransactionMessage.compileV0, куди передається масив об'єктів AddressLookupTableAccount:

У коді: створюється масив інструкцій, де в полях keys замість об'єктів PublicKey використовуються об'єкти { pubkey: PublicKey, isSigner: boolean, isWritable: boolean }. Потім викликається TransactionMessage.compileV0(message, [lookupTableAccount]). Результат — об'єкт MessageV0, у якому адреси, що є в таблиці, замінені на індекси.

Крок 5. Перевірка розміру.

Серіалізуйте отриману VersionedTransaction і перевірте довжину буфера. Для порівняння: серіалізуйте legacy-транзакцію з тими самими 20 акаунтами.

Параметр Legacy-транзакція V0 з ALT
Адреси в account_keys 20 × 32 = 640 байтів 20 × 1 = 20 байтів (індекси)
Довідник таблиць 0 байтів 2 байти (кількість) + 1 × 34 байти (адреса таблиці + індекс)
Чиста економія на адресах — ~584 байти

Реальна економія буде трохи меншою через додаткові поля заголовка v0 транзакції (префікс версії, кількість address table lookups), але чистий виграш для 20 акаунтів становить понад 570 байтів. Це може бути критичним, коли транзакція наближається до ліміту 1232 байти і додавання ще однієї інструкції без ALT неможливе.

Крок 6. Валідація перед відправкою.

Перед підписанням та відправкою викличте connection.simulateTransaction(versionedTransaction). Це дозволить виявити помилки lookup (наприклад, невідповідність індексів або відсутність адреси в таблиці) без витрат SOL на комісії.

Ризики та мітигація.

  • Таблицю деактивовано або закрито. Усі транзакції в польоті, що посилаються на цю таблицю, будуть відхилені. Мітигація: моніторинг стану таблиці через WebSocket-підписку на зміни облікового запису. Якщо fallback необхідний — готуйте паралельно legacy-версію транзакції (якщо вона вкладається в 1232 байти) або розбийте логіку на кілька транзакцій.
  • Зміна складу акаунтів. Якщо ваш застосунок починає використовувати нові акаунти, яких немає в таблиці, їх доведеться додати через extend. Це окрема транзакція з власною комісією та затримкою на один слот.
  • Залежність від конкретного слота. Транзакції з lookup прив'язані до стану таблиці на конкретний момент. Це не створює проблем у нормальних умовах, але ускладнює відтворення транзакцій для аудиту — потрібно відновлювати стан таблиці на момент її виконання.

Наступний логічний крок: якщо після оптимізації розміру транзакції через ALT ви стикаєтеся з обмеженнями на рівні паралельного запису в облікові записи, перейдіть до матеріалу про те, як обробляти concurrent writes до облікових записів.

Джерела