Роботи / Крипторобота на відкритий бриф

Inside the Block: як лідери Solana впорядковують транзакції всередині блоку

Що було

Superteam Ukraine поставив питання: за яким правилом валідатор Solana розставляє транзакції всередині блоку, за комісією чи за порядком приходу, і чи залежить це від клієнта. Умови: не менше 6 валідаторів, 3 версії клієнта, тільки мейннет і тільки після оновлення Alpenglow, витрати обмежені в коді, одна команда для перевірки.

Що зробили

Написали відправник, що шле свої копійчані транзакції у 4-слотове вікно обраного лідера, міняючи комісію і момент відправки, а потім читає блок і дивиться, яка стала першою. Провели 4 серії, 613 прогонів, 1 498 проб, блоки 119 лідерів. Правила вирішення для головного тесту закомітили до першої проби. Усе зібрали в Docker: одна команда без ключа перевіряє дані проти мейннету.

Що отримав клієнт

Відповідь з цифрами: транзакції, що приходять з розривом у десятки мілісекунд, лягають у порядку приходу в усіх клієнтах. Одночасні BAM-лідери ставлять дорожчу першою у 21 з 23 і 25 з 29 прогонів, p < 0,001. Jito-Agave без BAM і Firedancer правила не показали, і ми так і написали. Усі 1 328 проб, що потрапили в блоки, збігаються з getBlock.

ЗамовникSuperteam Ukraine, відкритий бриф
Моя рольДизайн вимірювання, код відправника і аналізу, серії на мейннеті, статистика, звіт, Docker-збірка, README
СтатусПодано, підсумки 19.11.2026. Серія 5 ще збирає дані
Результат21 з 23 прогонів у BAM-лідерів дорожча транзакція стала першою, p < 0,001 · Відкрити
Відхилено: підрахунок парами. 13 проти 5 за «дешевші першими», але всі 18 пар із трьох блоків одного лідера. Реконструкція, не скриншот
Відхилено: підрахунок парами. 13 проти 5 за «дешевші першими», але всі 18 пар із трьох блоків одного лідера. Реконструкція, не скриншот
Прийнято: один голос на блок, а в серії 4 по дві проби на прогін, тож прогін і є одним спостереженням. Реконструкція, не скриншот
Прийнято: один голос на блок, а в серії 4 по дві проби на прогін, тож прогін і є одним спостереженням. Реконструкція, не скриншот

Завдання

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 про те, за якою мірою комісії сортує планувальник. Допишемо, коли буде.

Помилки і що винесли

Підрахунок парами завищив упевненість.Чотири проби в одному блоці дають до шести пар, і всі вони ділять умови одного блоку. Один клас виглядав так, ніби ставить дешевші першими, 13 пар проти 5, але всі 18 пар були з трьох блоків. Тепер блок голосує один раз, а в серії 4 прогін і є одне спостереження.
Один RPC відставав від кластера на 2-6 слотів, одного разу на 50.Проби вилітали, коли вікно вже минуло. Годинник тепер якориться на найвищому слоті з кількох точок і інтерполює між читаннями.
Підписали проби заздалегідь, і вони померли дорогою.Blockhash живе близько 150 слотів, вікно буває за хвилини. Підписуємо за 10 слотів до вікна.
Серію 4 розмірили неправильно.План вимагав n не менше 10 на комірку, а 4 повтори при 60 % влучань у блок цього не давали. Помітили після 134 прогонів, розширили вирішальні комірки на 20 прогонів. Правила не чіпали, а в додатку до плану написали, що рішення ухвалене після проміжного погляду.
Дві серії дев'ять разів записали один файл прогону.Серії 3 і 4 йшли паралельно до тих самих лідерів і обирали те саме наступне вікно. 9 прогонів втрачено, їхні комісії лишились у журналі витрат. Викидання вцілілих 9 вердиктів не міняє, а файли тепер отримують унікальне ім'я.
Формат транзакцій v1 зламав читання блоків.Мейннет уже несе v1, де пріоритетна комісія лежить в окремому полі, а getBlock зі старою версією віддає помилку. Перейшли на версію 1 і читаємо ту комісію, яку справді списав леджер.

Інструменти

PythonSolana JSON-RPCDocker ComposepytestMemo-програма SolanaClaude Code