Кто может подменить код, работающий в Solana: замер 65 119 программ

от автора

Дисклеймер. Я пишу линтер для программ Solana и Anchor, и этот замер вырос из работы над ним. Английский оригинал лежит на сайте инструмента — ссылку даю в конце, чтобы не было вопросов о происхождении текста, но здесь статья полная: все числа, обе таблицы, метод и ограничения ниже, ходить никуда не нужно. Публикую сюда ровно за одним: за критикой методики. Последний раздел — список мест, где я сам считаю её слабой.


Аудит описывает код, который существовал в день, когда аудит писали. В Solana программу можно заменить, не мигрируя состояние, — и это делают. 29 июля 2026 года я задал mainnet два вопроса про каждую обновляемую программу: кто имеет право заменить этот код и как давно это делали в последний раз.

Ответы получены по 65 119 аккаунтам программ и по 348 программам, которые я наблюдал реально вызванными.

Коротко:

  • 49% из ста самых нагруженных программ обновляются одной парой ключей, а не он-чейн-мультисигом;

  • 13 дней — медианный возраст последнего деплоя у этой же сотни;

  • 3% программ отказались от возможности меняться вообще — authority отозван.

Неизменяемость в Solana — не норма, а погрешность округления. С чем бы вы ни взаимодействовали, кто-то почти наверняка сохраняет право это заменить, и по текущим темпам этим правом пользуются часто.

Зачем это вообще мерить

Обновляемый загрузчик Solana — это фича, а не дефект. Возможность выкатить фикс без миграции состояния — причина, по которой протокол реагирует на инцидент за часы, а не за недели. Никто в здравом уме не утверждает, что каждую программу надо замораживать на первый день.

Но у этого есть следствие, которое легко сформулировать и которое почти никогда не считают: аудит описывает конкретный бинарник, и у этого бинарника есть срок годности, который нигде не записан. Если вы прочитали аудит протокола, проинтегрировались и пошли дальше — вопрос о том, что по этому адресу лежит сегодня, это отдельный вопрос. Замер — попытка сказать, насколько отдельный.

Второй вопрос, кто держит ключ, решает, насколько быстро ошибка становится необратимой. Программу за он-чейн-мультисигом может заменить только кворум, состав и порог которого любой желающий читает из чейна. Программу за одной парой ключей может заменить тот, у кого эта пара.

Это не одна популяция, а две

Первое, что говорят данные: «программы в Solana» — не единая совокупность.

Из случайных 2 000, взятых среди 65 119 аккаунтов обновляемых программ, только 610 вообще имеют живой аккаунт ProgramData. Остальные 70% закрыты или брошены. Медианная программа среди этих 610 задеплоена 353 дня назад. Это кладбище, и любая статистика, посчитанная по нему, описывает мёртвых.

Поэтому я померил вторую популяцию: каждую программу, появившуюся в реальной инструкции — верхнеуровневой или вложенной — в 25 выбранных блоках. Получилось 348 различных программ на 132 563 вызовах: сеть в том виде, в каком её реально используют, взвешенная по использованию. Восемь из 348 — нативные программы без ProgramData (ComputeBudget, Vote, System, Associated Token, Memo, Ed25519, Secp256k1); это проверка метода, а не дыра в нём.

Обе популяции есть в каждой таблице ниже, потому что разрыв между ними — сам по себе результат.

Кто может подменить код

Upgrade authority — это либо обычный публичный ключ, либо program-derived address (PDA), либо его нет вовсе. Три случая различаются надёжно и без доверия к каким-либо меткам: PDA по построению не является валидной точкой кривой ed25519 — именно это и делает его неподписываемым приватным ключом.

Значит: authority, лежащий на кривой, — тот, за который может подписать пара ключей; authority вне кривой — тот, который может авторизовать только программа; отсутствующий authority — программа неизменяема.

Популяция

Программ

Одна пара ключей

Под контролем программы

Неизменяемых

Топ-25 по вызовам

25

13 (52%)

11 (44%)

1

Топ-50 по вызовам

50

28 (56%)

21 (42%)

1

Топ-100 по вызовам

100

49 (49%)

48 (48%)

3

Все вызванные

340

215 (63%)

114 (33%)

11

Случайная выборка из 65 119

610

538 (88%)

56 (9%)

16

Solana mainnet, 29 июля 2026, слот 435 941 295. Проценты — от разрешённых программ в каждой строке.

Смысл в градиенте. Брошенные программы почти сплошь одноключевые, и это никого не удивляет: хобби-деплои и мёртвые эксперименты. Дисциплина хранения ключей резко улучшается по мере того, как программу реально используют. И улучшается она примерно до подбрасывания монетки: среди сотни программ, несущих больше всего трафика в Solana, 49 не стоят за он-чейн-мультисигом.

Одно число одинаково во всех популяциях, и его я как раз не ожидал: неизменяемость держится на 3% везде. Почти ничто в Solana никогда не отказывалось от своего upgrade authority. Среди самой нагруженной сотни заметное исключение — сам SPL Token, у него authority отозван.

Как часто код меняется

Сначала свежесть. Возраст считается по измеренной длительности слота 0,3976 с, откалиброванной на 50 миллионах слотов, а не взятой из номинальных 400 мс.

Популяция

Медианный возраст

≤ 30 дней

≤ 90 дней

Топ-25 по вызовам

13 д

19 (76%)

24 (96%)

Топ-50 по вызовам

12 д

35 (70%)

46 (92%)

Топ-100 по вызовам

13 д

60 (60%)

81 (81%)

Все вызванные

25 д

178 (52%)

254 (74%)

Случайная выборка из 65 119

353 д

62 (10%)

144 (23%)

Дней с последнего деплоя и доля каждой популяции, передеплоенная в пределах 30 и 90 дней.

Снимок возрастов легко прочитать неправильно, поэтому я проверил его вторым способом: для 60 самых нагруженных программ выгрузил год транзакций, затрагивающих их аккаунты ProgramData.

Записей за год

Программ

0 — не трогали

3 (5%)

1–3

2 (3%)

4–11

15 (25%)

12 и больше

40 (66%)

Успешные записи в ProgramData за последние 365 дней, для 60 самых вызываемых программ. Медиана 20, среднее 56,6, максимум 468.

Два независимых метода — одномоментный снимок возрастов и год истории — сходятся по порядку величины. Нагруженная программа Solana заменяется примерно раз в две-три недели, и две трети самых нагруженных заменяются не реже раза в месяц. Код, на котором работает сеть, — не фиксированный артефакт, а движущийся.

Чего эти числа НЕ показывают

Этот раздел я сознательно поставил до интерпретации, а не после. У каждого числа выше есть граница, и цитировать число без его границы — значит сделать замер хуже, чем бесполезным.

«На кривой» не означает «один человек». Это означает «не за он-чейн-мультисигом». Ключ, хранимый в MPC или в пороговой схеме — Fireblocks, Turnkey и подобные, — требует подписи нескольких сторон и при этом выглядит в чейне как обычная пара ключей. Снаружи я этого не вижу, и часть из тех 49 наверняка так и устроена. Читайте цифру как верхнюю границу односигнатурного контроля, но никогда — как утверждение, что один человек может действовать в одиночку.

«Вне кривой» не означает «мультисиг». Это означает «под контролем программы». Squads — частый случай, но проверка не определяет, какая именно это программа, а PDA плохо управляемой программы не безопаснее аккуратно хранимого ключа.

25 блоков — это около десяти секунд содержимого mainnet, взятых с шагом 40 слотов в окне примерно 6,6 минуты. Популяция вызванных смещена в сторону высокочастотного трафика: DEX, оракулы, маркетмейкеры. Программу, которую вызывают несколько раз в день, она пропустит. Срез по топ-100 к этому устойчив, а вот 63% в строке «все вызванные» — нет, и цитировать это как общесетевой показатель нельзя.

Запись в ProgramData — верхняя граница числа передеплоев. Туда же пишут передача authority и расширение программы, и максимум в 468 неправдоподобен как 468 релизов. Медиана 20 подтверждается независимым снимком возрастов; среднему доверять нельзя, и я привожу его только для полноты.

Это одна точка во времени, снятая с одного RPC-эндпоинта, без сверки со вторым провайдером. Это снимок, и он будет плыть — что является причиной повторять замер, а не поводом его обесценивать.

Список одноключевых программ я намеренно не публикую. Агрегат — это исследование, а поимённый список — это список целей. Метода ниже достаточно, чтобы любой проверил свои собственные зависимости, и именно для этого я его и выкладываю.

Что из этого следует, если вы интегрируетесь

Возьмите протокол, который композится с восемью внешними программами — ничем не выдающееся число для чего угодно, занимающегося роутингом, лендингом или ликвидным стейкингом. По измеренным темпам примерно пять из этих восьми будут заменены в течение месяца с того дня, когда вы их посмотрели.

Это не значит пять враждебных изменений. Почти всё это — обычная инженерия: правка комиссий, оптимизация compute, новые инструкции. Но это значит, что фраза «мы проверили свои зависимости» протухает достаточно быстро, чтобы к ней требовалась дата, и что аудит, заказанный в апреле, описывает апрель.

Практические выводы скучные и дешёвые:

  • Записывайте слот деплоя, а не только адрес. «Мы интегрировали программу X» — невоспроизводимое утверждение; «мы интегрировали программу X в состоянии слота N» — воспроизводимое.

  • Знайте, какие из ваших зависимостей одноключевые. Это один RPC-запрос и одна проверка точки на кривой на адрес, и это меняет представление о том, какая доля вашего риска лежит вне вашего репозитория.

  • Спрашивайте про путь обновления до интеграции — ровно так же, как вы спросили бы про админские ключи у любой другой системы.

А тем, кто выкатывает программу, а не потребляет её: он-чейн-мультисиг — это работа на выходные, и он переводит вас из 49% в 48%. Полный отзыв authority — решение куда более крупное, и этот замер не является аргументом за него: 97% сети от него отказались, в основном по хорошим причинам.

Метод

Популяция — каждый аккаунт, принадлежащий BPFLoaderUpgradeab1e11111111111111111111111, с длиной данных ровно 36 байт: 4-байтовый enum плюс адрес ProgramData. Размер служит точным предикатом «это программа». Так находится 65 119 аккаунтов.

Адрес ProgramData каждой программы — это PDA от program id под загрузчиком, поэтому он выводится локально, а не ищется запросом. Первые 45 байт — дискриминант, u64 слота последнего деплоя и Option<Pubkey> authority; они читаются по 100 штук за раз через data slice, так что вся перепись стоит несколько сотен запросов, а не десятки тысяч.

Authority классифицируются декомпрессией точки ed25519, как описано выше. Соответствие слота времени измеряется, а не предполагается. История передеплоев берётся постраничным getSignaturesForAddress по аккаунту ProgramData с подсчётом успешных транзакций за последний год.

Перепись — один Python-скрипт без зависимостей, docs/measurements/scripts/mainnet-census.py в репозитории. Каждый параметр, способный сместить результат — сид рандома, число и шаг блоков, интервал калибровки, — вынесен во флаг, и его дефолт равен значению, использованному здесь, так что более поздний запуск сопоставим с этим.

./mainnet-census.py population./mainnet-census.py active./mainnet-census.py headers active./mainnet-census.py headers all 2000./mainnet-census.py report./mainnet-census.py churn 60

Агрегированные результаты в машиночитаемом виде — CSV, одна строка на популяцию, только счётчики, под CC BY 4.0.

Ссылки для перепроверки:

Где я сам считаю метод слабым

Собственно, ради этого раздела я и пишу. Четыре места, где мне не нравится то, что получилось, — и я буду рад, если их разберут в комментариях.

1. Бакет «под контролем программы» — чёрный ящик, и это худшая дыра. 114 программ имеют authority вне кривой, и я останавливаюсь на этом. Но мультисиг-программа сама по себе обновляемая. Если Squads апгрейдится одним ключом, то часть моих «48% под контролем программы» — это те же 49% через один уровень косвенности, и настоящее число односигнатурного контроля выше заявленного.

Определить контролирующую программу по полю owner аккаунта не выходит: у vault-PDA в Squads v4 владелец — System Program, а незафондированный PDA может вообще не иметь аккаунта. Единственный путь, который я вижу, — брать известные мультисиг-программы (Squads v3/v4, Goki, SPL Governance), выводить кандидатов-PDA по их схемам сидов и матчить с наблюдаемыми authority. Это перебор по конечному, но неполному списку, и любая программа вне списка останется неопознанной. Если есть способ лучше — я его не нашёл.

2. Запись в ProgramData ≠ передеплой, и я не стал это разделять. Мне кажется, это чинится разбором дискриминанта инструкции загрузчика (Upgrade, SetAuthority, ExtendProgram) в каждой транзакции — тогда максимум 468 превратится в осмысленное число релизов вместо верхней границы. Я этого не сделал: это заметно дороже по запросам. Вопрос — стоит ли оно того, и не наступлю ли я там на что-то неочевидное.

3. 25 блоков. Берутся они не подряд: шаг 40 слотов, так что выборка растянута примерно на 6,6 минуты, а суммарно осмотрено около десяти секунд содержимого блоков. Разрежённость немного помогает, но окно всё равно одно и короткое — суточного профиля в нём нет, и выборка смещена в сторону высокочастотного трафика. Правильнее была бы стратифицированная выборка, разнесённая по суткам. Меня останавливает стоимость по RPC. Интересно, есть ли у кого-то оценка, насколько сильно смещение на самом деле.

4. Один RPC-провайдер, ноль сверок. Тут всё просто: это плохо, я это знаю, и в следующем квартале хочу гонять замер против двух эндпоинтов и сравнивать. Если кто-то знает случаи, когда публичные RPC расходятся в данных аккаунта, — расскажите, это ровно то, что я собираюсь ловить.

И один вопрос, на который у меня нет даже плохого ответа: есть ли хоть какая-то он-чейн-эвристика, отличающая ключ в MPC от ключа в одном файле? Я думаю, что нет, и что это принципиально ненаблюдаемо снаружи. Но если я неправ, это самая ценная поправка ко всему замеру.

Замер я собираюсь повторять ежеквартально. Следующий — Q4 2026, и интересным там будет не какое-то отдельное число выше, а то, сдвинутся ли эти 49%.

ссылка на оригинал статьи https://habr.com/ru/articles/1065006/