Завдання
Superteam Ukraine відкрив бриф із одним питанням: як валідатори Solana впорядковують транзакції всередині блоку. За комісією, за порядком приходу чи взагалі без правила, і чи різниться це між клієнтами. Умови жорсткі: щонайменше 6 валідаторів і 3 версії клієнта, тільки мейннет, усі заміри після оновлення Alpenglow, ліміт витрат у коді, ключі тільки через оточення, одна команда для запуску, сирі дані з підписами і слотами, а в звіті окремий розділ «що пішло не так».
Складність у тому, що між нашим бекендом і планувальником лідера стоять публічні RPC, QUIC-з’єднання і, у Jito та BAM, ще один вузол. Ми бачимо, коли відправили і де транзакція опинилась, а що відбувалося дорогою, ні.
Навіщо і яка ідея
Замість читати чужий код планувальника ми вирішили міряти поведінку ззовні. Наші проби: переказ одного лампорта самому собі з різною пріоритетною комісією. Шлемо їх у 4-слотове вікно обраного лідера, потім читаємо блок і дивимось, яка стала першою. Якщо дорожча лягла першою, хоч відправлена пізніше, лідер ранжує за комісією. Якщо перша відправлена стає першою незалежно від ціни, лідер працює в порядку приходу. Якщо ні те, ні інше, це шум.
Два рішення визначили чесність результату. Перше: правила, за якими серія 4 виносить вердикт «fee», «no fee rule» або «undetermined», ми записали в ANALYSIS_PLAN.md і закомітили до першої проби, історія git показує порядок. Друге: для серії 5 хеш плану записали в мейннет через Memo-транзакцію ще до того, як з’явився код серії.
Одне рішення переглянули по ходу. Спочатку хотіли впізнавати Jito-лідерів за переказами на tip-акаунти в їхніх блоках. Такі перекази виявились у кожному блоці, у Firedancer теж, бо переказ на tip-акаунт може зробити будь-хто. Перейшли на публічний список валідаторів Jito.
Хід роботи
| Коли | Що відбувалося |
|---|---|
| Серія 1 | 30 прогонів за версіями клієнта, послідовна відправка в порядку зростання, спадання і випадково. Комісії до 60 лампортів. Перші 9 прогонів ще до виправлення годинника слотів |
| Серія 2 | 67 прогонів, дев’ять фіксованих лідерів, по одному на клас клієнта. Пакетна і послідовна відправка по черзі, комісії підняли у сто разів, до 6 000 лампортів |
| Серія 3 | 39 прогонів, тільки пакетна відправка до семи частих лідерів, бо для блокового тесту бракувало блоків на клас |
| Серія 4, та сама ніч | 477 прогонів: дві проби на прогін, хто перший вирішує монетка, розрив 0, 5, 20, 50 або 150 мс, окремі платники або один спільний акаунт із write-lock. Правила закомічені до старту, через півтори години серію розширили на 20 прогонів у вирішальних комірках |
| Ранок | Повна звірка: 785 блоків перечитані через getBlock, усі 1 328 проб на своїх місцях. Docker перевірений із чистого клону на сервері, README і REPORT написані, репозиторій відкритий |
| Через тиждень | Подача. Того ж дня закомічено план серії 5, його SHA-256 записано в мейннет Memo-транзакцією, серія 5 запущена на чотири класи клієнтів окремими процесами |
Чим робили
Код на Python, без фреймворків: survey.py збирає розклад лідерів, версії клієнтів і прапорці Jito та BAM; sender.py будує проби, тримає локальний годинник слотів і шле у вікно; budget.py веде журнал витрат із файловим замком, щоб дві паралельні серії ділили один ліміт 0,05 SOL; analyze.py і report.py знаходять слот, індекс у блоці й лідера, який його справді виробив, і рахують статистику з точним знаковим тестом і поправкою Холма.
Проби йшли через два публічні RPC одночасно, з повтором кожні 150 мс. Підписувати наперед не можна: blockhash живе близько 150 слотів, а вікно потрібного лідера може бути за хвилини. Тому відправник спить до 10 слотів перед вікном, бере свіжий blockhash, підписує і починає слати за 3 слоти до нього.
docker compose up не потребує ключа і нічого не витрачає: проганяє тести, перебудовує таблиці з сирих даних, перечитує 10 випадкових прогонів через getBlock і робить сухий прогін на мейннеті без відправки. Ключі лише з файлів, названих в оточенні, у репозиторії їх немає.
Результат
Відповідь у три рядки. Транзакції, що досягають лідера з розривом у десятки мілісекунд, лягають у порядку приходу, хоч який клієнт: 4.3.2 Jito 17 з 18 блоків, Firedancer 12 з 12, BAM 11 з 13. Транзакції, що приходять разом, BAM-лідери сортують за комісією: дорожча перша у 21 з 23 прогонів з окремими акаунтами і 25 з 29 зі спільним, 13-14 різних лідерів, p < 0,001 з поправкою Холма. Firedancer і Jito-Agave без BAM схиляються до комісії, але порогу не проходять, і ми називаємо це «undetermined», а не натягуємо результат.
Масштаб чесно: 613 прогонів, 1 498 проб, 1 328 потрапили в блоки, 673 з них у ціль, решта до сусідніх лідерів. Аналіз відносить кожну пробу до лідера, який справді виробив блок. Зарезервовано під лімітом 0,0113 SOL із дозволених 0,05.
У звіті 14 пунктів «що пішло не так»: відстаючий RPC-годинник, протухлі підписи, замалі комісії в серії 1, дві серії, що дев’ять разів потрапили в одне вікно, і хибний «random», де зерно було зафіксовано нулем.
Чого ми поки не знаємо: як бриф оцінить роботу і що покаже серія 5 про те, за якою мірою комісії сортує планувальник. Допишемо, коли буде.
Помилки і що винесли



