
Проверяемое доказательство для CVE-2022-40769: лежит ли приватный ключ EOA в пространстве ключей, достижимых через Profanity?
Доказуемая проверка CVE-2022-40769: лежит ли приватный ключ EOA в пространстве ключей, достижимом генератором красивых адресов Profanity? Если да, ключ восстановим, и счёт (и всё, что он контролирует) может быть опустошён кем угодно.
Никаких эвристик. Результат — булево значение, подкреплённое полным перебором точного пространства ключей, воспроизводимым при повторном запуске того же шарда.
Dispatcher.cpp, createSeed():
std::random_device rd;
std::mt19937_64 eng(rd()); // seeded with only 32 bits <- CVE-2022-40769
std::uniform_int_distribution<cl_ulong> distr;
cl_ulong4 r;
r.s[0] = distr(eng); r.s[1] = distr(eng); // 256-bit seed, used directly
r.s[2] = distr(eng); r.s[3] = distr(eng); // as the private key
Затем ядро OpenCL берёт G^k для этого ключа и идёт вперёд, увеличивая ключ на единицу за раунд, пока адрес не совпадёт с запрошенным шаблоном. Таким образом, всё достижимое пространство ключей таково:
key(x, i) = mt19937_64(x) as a 256-bit scalar + i
x ∈ [0, 2^32) the 32-bit seed (four billion possibilities)
i ∈ [0, depth] rounds the generator ran
P_j = P − j·G для j ≤ depth. Если ключ был создан Profanity, одна из этих точек является публичным ключом сида.x выводит mt19937_64(x) и его адрес.P_j — это публичный ключ сида, поэтому приватный ключ кандидата равен seed(x) + j — восстановим, следовательно, счёт можно опустошить.Совпадение — это доказательство. Отсутствие совпадения по всему диапазону сидов — доказательство обратного, вплоть до выбранной глубины.
std::mt19937_64 для сидов 0, 1, 12345 и 4294967295 были сравнены лимб за лимбом с программой на g++, использующей std::mt19937_64 + std::uniform_int_distribution<unsigned long long>; идентичны.k256 (чистый Rust) и secp256k1 (привязки libsecp256k1): сид 0 → 0xfef2583edde5637dad990bc5d05d52c8247019cf, сид 12345 → 0x7712c45360f5dfa3a622a815b6073f0351050f13.--selftest проверяет вывод ключа, адрес, обход цепочки, восстановление обходом вниз и сквозное сканирование сидов, которое находит известный сид.Запустите сами:
cargo run --release -- --selftest
profanity-verifier --address 0x... [--pubkey 0x...] --shard i/n [--depth 16777216]
--pubkey — несжатый публичный ключ кандидата (64 байта x||y, с префиксом 0x04 или без него). Восстановите его из любой транзакции, подписанной адресом. Без него можно проверить только позицию 0 в цепочке.--shard i/n — срез 32-битного пространства сидов для поиска, чтобы работу можно было распараллелить между машинами.--depth — насколько далеко по цепочке генератора искать. 2^24 покрывает шестисимвольный красивый адрес с запасом; большая глубина стоит памяти и времени.Вывод — одна строка JSON, например:
{"address":"0x...","shard":"0/64","depth":16777216,"seeds_checked":67108864,"seconds":833.4,"match":true}
Сид и приватный ключ никогда не выводятся и не сохраняются. Выдаются только булево значение, адрес и шард, поэтому результат можно опубликовать, никому не передавая ключ.
.github/workflows/verify.yml распределяет поиск по матрице заданий (candidate, shard) на публичных раннерах.
Измеренная пропускная способность: ~20 000 сидов/с на 2 ядрах (libsecp256k1). Полное пространство сидов 2^32 — это ~59 ядро-часов, поэтому 64 шарда × 4 ядра ≈ 14 минут реального времени на кандидата — бесплатно в публичном репозитории.
Запуск со списком кандидатов:
gh workflow run profanity-verify -f candidates='[{"address":"0x...","pubkey":"0x..."}]' -f shards=64
std::mt19937_64 из libstdc++, что покрывается сравнением лимб за лимбом. Сборки с другой стандартной библиотекой имели бы другое отображение.depth ограничивает поиск. Кандидат, сгенерированный после необычно долгого поиска (очень длинные шаблоны красивых адресов), может находиться за его пределами.