
Vérification prouvable pour CVE-2022-40769 : la clé privée d'une EOA se trouve-t-elle dans l'espace de clés atteignable par Profanity ?
Vérification prouvable de CVE-2022-40769 : la clé privée d'une EOA se trouve-t-elle dans l'espace de clés atteignable par le générateur d'adresses vanity Profanity ? Si oui, la clé est récupérable et le compte (ainsi que tout ce qu'il contrôle) peut être vidé par n'importe qui.
Aucune heuristique. Le résultat est un booléen appuyé par une recherche exhaustive de l'espace de clés exact, reproductible en relançant le même shard.
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
Le kernel OpenCL prend ensuite G^k pour cette clé et avance, incrémentant la clé de un par tour jusqu'à ce que l'adresse corresponde au motif demandé. L'espace de clés atteignable entier est donc :
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 pour j ≤ depth. Si Profanity a fabriqué la clé, l'un de ces points est une clé publique de seed.x, il dérive mt19937_64(x) et son adresse.P_j est la clé publique de seed, donc la clé privée du candidat est seed(x) + j — récupérable, donc vidable.Une correspondance est une preuve. L'absence de correspondance sur toute la plage de seeds est la preuve du contraire, jusqu'à la profondeur choisie.
std::mt19937_64 pour les seeds 0, 1, 12345 et 4294967295 ont été comparés membre par membre à un programme g++ utilisant std::mt19937_64 + std::uniform_int_distribution<unsigned long long> ; identiques.k256 (Rust pur) et secp256k1 (bindings libsecp256k1) : seed 0 → 0xfef2583edde5637dad990bc5d05d52c8247019cf, seed 12345 → 0x7712c45360f5dfa3a622a815b6073f0351050f13.--selftest vérifie la dérivation, l'adresse, le parcours de chaîne, la récupération par remontée à rebours, et un scan de seeds de bout en bout qui trouve un seed connu.Lancez-le vous-même :
cargo run --release -- --selftest
profanity-verifier --address 0x... [--pubkey 0x...] --shard i/n [--depth 16777216]
--pubkey — la clé publique non compressée du candidat (64 octets de x||y, avec ou sans le préfixe 0x04). Récupérez-la à partir de n'importe quelle transaction signée par l'adresse. Sans elle, seule la position 0 dans la chaîne peut être testée.--shard i/n — la tranche de l'espace de seeds 32 bits à parcourir, ce qui permet de paralléliser le travail entre machines.--depth — jusqu'où chercher le long de la chaîne du générateur. 2^24 couvre un vanity de six caractères avec de la marge ; plus profond coûte de la mémoire et du temps.La sortie est une seule ligne JSON, par exemple :
{"address":"0x...","shard":"0/64","depth":16777216,"seeds_checked":67108864,"seconds":833.4,"match":true}
Le seed et la clé privée ne sont jamais affichés ni stockés. Seuls le booléen, l'adresse et le shard sont émis, de sorte qu'un résultat peut être publié sans remettre la clé à quiconque.
.github/workflows/verify.yml répartit la recherche sur une matrice de jobs (candidate, shard) sur des runners publics.
Débit mesuré : ~20 000 seeds/s sur 2 cœurs (libsecp256k1). L'espace complet de 2^32 seeds représente ~59 heures-cœur, donc 64 shards × 4 cœurs ≈ 14 minutes de temps réel par candidat — gratuit sur un dépôt public.
Lancez avec la liste de candidats :
gh workflow run profanity-verify -f candidates='[{"address":"0x...","pubkey":"0x..."}]' -f shards=64
std::mt19937_64 de libstdc++, ce que couvre la comparaison membre par membre. Les builds contre une bibliothèque standard différente auraient une correspondance différente.depth borne la recherche. Un candidat généré après une recherche anormalement longue (motifs vanity très longs) peut se situer au-delà.