Дисклеймер. Я пишу линтер для программ 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.
Ссылки для перепроверки:
-
скрипт переписи:
mainnet-census.py -
полные рабочие заметки, включая проверки, прогнанные против результатов: запись замера
-
английский оригинал и CSV: vaultlint.com/reports/solana-program-upgradeability-2026-q3/
Где я сам считаю метод слабым
Собственно, ради этого раздела я и пишу. Четыре места, где мне не нравится то, что получилось, — и я буду рад, если их разберут в комментариях.
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/