- C++ 56.7%
- Python 37.6%
- Cuda 5.2%
- CMake 0.4%
- C 0.1%
|
|
||
|---|---|---|
| .agents | ||
| python | ||
| src | ||
| tests | ||
| tools | ||
| .git-blame-ignore-revs | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CMakeLists.txt | ||
| CMakePresets.json | ||
| COMANDOS.md | ||
| README.md | ||
⚡ cuda-labs
Aprendendo CUDA de verdade — de funções de hash a um pipeline secp256k1 completo que transforma chave privada em endereço Bitcoin a mais de 1 bilhão de chaves por segundo numa RTX 4060.
Começou como um punhado de exemplos de hashing na GPU e foi crescendo, um conceito por vez, até virar o núcleo de um buscador de chaves no estilo BitCrack / VanitySearch / Keyhunt — só que construído do zero, para aprender, com cada peça conferida contra uma referência independente. É um projeto pessoal e educativo: todos os testes buscam apenas chaves que já conhecemos (veja Escopo & ética).
✨ Destaques
- Hashing em lote na GPU — SHA-256, RIPEMD-160 e a cascata HASH160, cada um processando milhares de mensagens em paralelo.
- Pipeline secp256k1 completo — aritmética de corpo → curva elíptica → multiplicação escalar → inversão em lote → chave pública → endereço, tudo fundido num único kernel.
- Rápido de verdade — ~1.250 Mkeys/s sustentados (1,25 Gkey/s), no mesmo nível de referências consagradas do ramo.
- CLI Python bonita — barra de progresso ao vivo, tabela de puzzles, presets, retomada automática de onde parou, suíte de sanidade e um auto-tuner de GPU embutido.
- Correção levada a sério — cada camada é validada contra um oráculo independente em Python, então um bug sutil de sinal ou endianness não passa despercebido.
🧠 Como funciona (a ideia, sem matemática)
Descobrir o endereço a partir de uma chave privada é um caminho de mão única — fácil de ir, impossível de voltar. A busca simplesmente percorre um intervalo de chaves e refaz esse caminho para cada uma, comparando o resultado com o endereço-alvo:
chave privada k
│
▼ multiplicação na curva elíptica
k · G ─────────────► ponto (x, y) na curva secp256k1
│
▼ compressão (33 bytes)
chave pública
│
▼ SHA-256 → RIPEMD-160
HASH160 (20 bytes) ───────────► == alvo ? 🎯
O truque de desempenho é fazer tudo dentro de um só kernel, para milhares de chaves ao mesmo tempo, sem nunca voltar para a CPU no meio do caminho.
🚀 Começando rápido
Pré-requisitos (já instalados nesta máquina): GPU NVIDIA RTX 4060, CUDA Toolkit 13.3, Visual Studio 2026 (fornece o
cl.exe), CMake ≥ 3.24 e Ninja. Detalhes e por que essas versões em Aprofundando.Sem GPU NVIDIA? O CUDA é opcional — o projeto também compila e roda 100% na CPU. Veja Sem GPU? Modo CPU. Aí só o
cl.exe(ou qualquer compilador C++), o CMake e o Ninja são necessários.
1. Prepare o terminal (o nvcc precisa do cl.exe do Visual Studio no PATH):
& "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation
No VSCode, basta abrir um terminal com o perfil "PowerShell CUDA (VS Dev)" — ele já carrega esse ambiente.
2. Compile:
cmake -B build -G Ninja
cmake --build build
Ou, equivalente, pelos presets:
cmake --preset defaultecmake --build --preset default. Há também--preset profile(instrumentação para o Nsight, embuild-profile/) e--preset no-cuda(força a configuração só-CPU mesmo com o nvcc instalado).
O configure imprime
cuda-labs: build type = Release. Release é o padrão — sem isso o CMake escolheriaDebugsozinho no MSVC, o que não quebra nada mas custa ~6–8× no backend CPU. Para depurar, passe-DCMAKE_BUILD_TYPE=Debugexplicitamente.
3. Valide o build — os 13 drivers de conformidade, via CTest:
ctest --test-dir build --output-on-failure
4. Veja funcionando — a CLI recuperando a chave do puzzle #1:
Set-Location .\python
python main.py --preset puzzle-1-demo
🖥️ Usando a CLI
A CLI vive em python/ e fala com o backend CUDA por uma DLL (sem etapa de compilação do lado
Python). Instale as dependências uma vez e rode a partir de python/:
Set-Location .\python
python -m pip install -r requirements.txt # rich (+ opcionais: Telegram, httpx p/ --server-url)
📚 Procurando um comando pronto? A biblioteca de comandos (
COMANDOS.md) reúne as várias formas de rodar o buscador, cada uma com uma explicação curta. As seções abaixo explicam os recursos em detalhe.
Buscar uma chave
Escolha o alvo por número de puzzle (--wallet) ou por endereço/hash160 (--target). Ao
achar, a chave aparece em destaque (hex, decimal e WIF):
┌────────────────── keyhunt - run summary ──────────────────┐
│ Device NVIDIA GeForce RTX 4060 (sm_89, 24 SMs) │
│ Target 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH │
│ HASH160 751e76e8199196d454941c45d1b3a323f1433bd6 │
│ Puzzle #1 │
│ Range [0x1, 0x1] │
│ Tuning library defaults │
│ Already scanned 0.0000% (0 keys) │
└───────────────────────────────────────────────────────────┘
window 1 100%|██████████| 1.00/1.00 [3.1 Mk/s, ETA 0s]
┌─────────────────────────────── ** PRIVATE KEY FOUND ** ───────────────────────────────┐
│ Address 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH │
│ Private key (hex) 0x0000000000000000000000000000000000000000000000000000000000000001 │
│ Private key (dec) 1 │
│ WIF (compressed) KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn │
└───────────────────────────────────────────────────────────────────────────────────────┘
Numa busca longa a barra mostra %, velocidade instantânea, média e ETA ao vivo. O progresso
é salvo por alvo: se você parar com Ctrl-C e rodar de novo, ele retoma de onde parou em
vez de reescanear o mesmo trecho.
Começar numa chave exata do puzzle (--from). Com --target, o --from é o início do
intervalo. Com --wallet, ele é onde a busca sequencial começa, dentro do puzzle:
python main.py --wallet 71 --from 0x600000000000000000 -bs 1e9 -bc 100
O intervalo — e todas as porcentagens — continuam sendo os do puzzle inteiro; só o ponto de
partida muda (o painel de resumo ganha a linha Starts at). As chaves abaixo de --from não
são visitadas, então ao esgotar o trecho a mensagem é no free keys left from 0x6000… to the end of the range - swept., e não "fully scanned". Um --from fora do intervalo do puzzle é
recusado (não ajustado em silêncio). Como ele decide onde as janelas caem, segue as
mesmas regras do --chunks: desliga o rng herdado de um preset, e combiná-lo com --rng ou
--chunks é recusado. (Antes desta versão, --from com --wallet era aceito e ignorado.)
Com
--rng, cada janela cai num ponto aleatório do intervalo. O--rng-mode extendé uma variação que continua a partir do fim de um trecho já escaneado — as janelas se fundem no registro de progresso em vez de fragmentá-lo, mantendo o JSON com tamanho ~estável.
Quando a chave é achada, ela também fica marcada no arquivo de progresso do alvo, no campo
"found" (null enquanto ninguém achou; a chave em hex de 64 dígitos depois). É o que faz
uma execução posterior no mesmo alvo mostrar Key found: yes - recovered in an earlier run em vez de no — os intervalos escaneados sozinhos não conseguem dizer isso, porque um
acerto interrompe a janela no meio e deixa um intervalo parcial idêntico ao de um Ctrl-C.
O registro definitivo da chave (hex, decimal e WIF) continua sendo o data/results.json.
{
"address": "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH",
"hash160": "751e76e8199196d454941c45d1b3a323f1433bd6",
"found": "0000000000000000000000000000000000000000000000000000000000000001",
"updated": "2026-08-06T22:44:51+00:00",
"intervals": [["1","1"]]
}
Arquivos antigos (sem o campo) continuam carregando normalmente — a ausência é lida como
null. O--jointambém preserva o campo: se qualquer arquivo de entrada tiver a chave, ela sobrevive na saída.
Amostragem espaçada (--chunks)
As duas variantes de --rng sorteiam onde cada janela cai. --chunks N faz o contrário:
corta o intervalo em N faixas iguais e escaneia uma janela de --range-size chaves
no início de cada faixa. A faixa decide onde, o -bs decide quanto — nada de sorteio.
python main.py --preset wallet-71-spread # 1000 janelas de 1e9, espalhadas pelo 2^70
python main.py --wallet 71 --chunks 10 -bs 1e9 # 10 amostras, uma a cada 10% do intervalo
Num intervalo 0..999 com --chunks 10 e -bs 10, as janelas ficam em [0,9], [100,109],
[200,209], … [900,909].
- Faixa tocada é faixa encerrada. Rodar o mesmo comando de novo não escaneia nada — o modo amostra, não varre. Para continuar amostrando, aumente o N.
- Aumentar o N refina a grade. Depois de
--chunks 10, um--chunks 20encontra aqueles dez inícios já cobertos, pula, e escaneia só as dez posições novas no meio. Dá para adensar a amostragem quantas vezes quiser sem repetir trabalho. Nos puzzles, refine com potências de 2 (1024 → 2048): o intervalo tem 2^(n−1) chaves, e só assim as posições antigas coincidem exatamente com as novas — com 1000 → 2000 elas ficam deslocadas e cada faixa antiga ganha uma janela minúscula a mais. - Não guarda estado nenhum. "Esta faixa já foi?" é lido direto dos intervalos do arquivo de
progresso, então retomada,
--join,Ctrl-Ce--progress-suffixfuncionam sem nenhum caso especial. - Segura o tamanho do arquivo de progresso. O
--rngcria um intervalo por janela, para sempre;--chunks Nnunca passa de N intervalos. Num alvo antigo isso é a diferença entre um JSON de centenas de MB e um de alguns KB — e o arquivo é reescrito inteiro a cada gravação. - Não combina com
--rng: são duas formas de escolher o mesmo, então um--chunksna linha de comando desliga orngque veio de um preset (é a precedência normal, CLI > preset), e pedir os dois na mesma linha é recusado com uma mensagem, não resolvido em silêncio. - Um
Ctrl-Cno meio de uma faixa a encerra com uma amostra menor que o-bs, e ela não volta. É seguro — nada é marcado como escaneado sem ter sido —, só amostra menos do que você pediu. - A última faixa absorve o resto da divisão, e a janela é cortada no fim da própria faixa: um
-bsmaior que a faixa não vaza para a seguinte.--chunksmaior que o intervalo é recusado (uma faixa teria menos de uma chave). Vale como chave de preset ("chunks": 1000) e aparece no painel de resumo com o espaçamento calculado.
Começar de uma faixa específica (--chunk-start)
--chunk-start K escolhe em qual faixa começar. A numeração é a partir de 0: com
--chunks 100 as faixas vão de 0 a 99.
python main.py --wallet 71 --chunks 100 --chunk-start 90 -bs 1e9 # faixas 90, 91, ..., 99
- O percurso vai de
Katé a última faixa e para — não dá a volta para as faixas antes deK. Para amostrar essas depois, rode sem--chunk-start(as que já foram são puladas normalmente). - Continua sem estado: o
Ké parte do comando, não do arquivo de progresso. Repetir o mesmo comando retoma de onde parou (e, se tudo deKem diante já foi, não escaneia nada). -bccontinua valendo; o que acabar primeiro — as faixas ou as janelas — encerra a execução.- Sem
--chunks, ou fora de0..N-1, é recusado com uma mensagem (não ajustado em silêncio). Vale como chave de preset ("chunk_start": 90); aparece no painel de resumo, no--list-presets(chunks:100@90) e no Telegram.
Varrer em sequência a partir de uma faixa (--chunk-mode sweep)
Por padrão (--chunk-mode sample) as faixas são amostradas: uma janela no início de cada.
Com --chunk-mode sweep elas viram só uma grade de partida: a busca começa na primeira
chave da faixa K e segue em sequência, janela após janela, sem buraco nenhum —
atravessa para a faixa K+1, K+2, … até o fim do intervalo, e para.
python main.py --wallet 71 --chunks 100 --chunk-start 90 --chunk-mode sweep -bs 1e9
A barra nomeia cada janela como chunk 90.1, chunk 90.2, … chunk 91.1: o número depois do
ponto é a posição da janela dentro da faixa, em passos de -bs. Ele é calculado pela
posição, não contado — então, depois de retomar, a numeração continua de onde está
(chunk 90.37) em vez de recomeçar do .1.
- Retoma sozinho, sem estado. Chaves já escaneadas são puladas — inclusive as de outros
modos (as amostras do
--chunks, as janelas do--rng) —, então repetir o mesmo comando continua da primeira chave livre a partir da faixaK. UmCtrl-Cgrava só o trecho verificado, e a próxima execução começa exatamente depois dele. - Para no fim do intervalo com
no free keys left from chunk 90 to the end of the range - swept.— e não "fully scanned", porque as faixas antes deKnem foram olhadas. -bce-tllimitam a sessão como sempre (o que acabar primeiro). Num puzzle 71, uma faixa de--chunks 100tem ~1,18×10¹⁹ chaves — séculos a ~1,2 Gk/s —, então na prática a varredura é feita em sessões: cada execução continua de onde a anterior parou.--chunk-start 0comsweepé exatamente o modo sequencial normal.--chunk-modesem--chunksé recusado. Vale como chave de preset ("chunk_mode": "sweep") e aparece no painel de resumo, no--list-presets(sweep:100@90) e no Telegram (sweep x100 from #90, com a janelachunk 90.37no painel ao vivo).
Sweep ou
--from? Os dois começam uma varredura sequencial no meio de um puzzle. O sweep escolhe o ponto de partida por faixa (--chunk-start 90); o--fromescolhe a chave exata (--wallet 71 --from 0x600000000000000000) — veja Buscar uma chave.
Listar os puzzles
python main.py --list-puzzles
Bitcoin puzzles
┌─────┬─────────────────────────────┬───────────────┬──────────────┐
│ # │ Address │ Range │ Size │
├─────┼─────────────────────────────┼───────────────┼──────────────┤
│ 1 │ 1BgGZ9tcN4rm9KBzDn7KprQz87… │ [0x1, 0x1] │ 2^0 (1) │
│ 2 │ 1CUNEBjYrCn2y1SdiUMohaKUi4… │ [0x2, 0x3] │ 2^1 (2) │
│ 3 │ 19ZewH8Kk1PDbSNdJ97FP4EiCj… │ [0x4, 0x7] │ 2^2 (4) │
│ 4 │ 1EhqbyUMvvs7BfL8goY6qcPbD6… │ [0x8, 0xf] │ 2^3 (8) │
│ 5 │ 1E6NuFjCi27W5zoXg8TRdcSRq8… │ [0x10, 0x1f] │ 2^4 (16) │
│ 6 │ 1PitScNLyp2HCygzadCh7FveTn… │ [0x20, 0x3f] │ 2^5 (32) │
└─────┴─────────────────────────────┴───────────────┴──────────────┘
(… e assim por diante até o puzzle #160.)
Ajustar a GPU automaticamente (--tune)
Faz um grid search de lanes × stride × gpu-batch, mede a velocidade de cada combinação e
mostra a melhor — com aquecimento, mediana entre waves e ordem alternada para não ser enganado
por variação térmica, tudo automático:
python main.py --tune --wallet 71
tuning results (top 4 of 4)
┌───┬───────┬────────┬───────────┬─────────┬────────┬───────┐
│ # │ lanes │ stride │ gpu-batch │ Mkeys/s │ passes │ clock │
├───┼───────┼────────┼───────────┼─────────┼────────┼───────┤
│ 1 │ 32768 │ 6144 │ 384 │ 1074.3 │ 1 │ 2865 │
│ 2 │ 32768 │ 6144 │ 256 │ 1018.0 │ 1 │ 2865 │
│ 3 │ 32768 │ 2048 │ 384 │ 979.5 │ 1 │ 2865 │
│ 4 │ 32768 │ 2048 │ 256 │ 966.8 │ 1 │ 2865 │
└───┴───────┴────────┴───────────┴─────────┴────────┴───────┘
Os resultados também vão para benchmark_results/ (CSV + JSON). Depois é só passar o combo
vencedor para a busca com --lanes / --stride / --gpu-batch.
Conferir que tudo funciona (--test)
Recupera 42 chaves de puzzles já resolvidos (puzzles 1 a 42) e confere cada WIF contra uma implementação independente — um teste de ponta a ponta do backend, sem efeitos colaterais:
┌────┬───────────┬───────────┬─────┬─────────┬────────┐
│ # │ key (hex) │ recovered │ wif │ Mkeys/s │ result │
├────┼───────────┼───────────┼─────┼─────────┼────────┤
│ 1 │ 1 │ ok │ ok │ 291 │ PASS │
│ 2 │ 3 │ ok │ ok │ 320 │ PASS │
│ 3 │ 7 │ ok │ ok │ 410 │ PASS │
│ 4 │ 8 │ ok │ ok │ 417 │ PASS │
│ 5 │ 15 │ ok │ ok │ 318 │ PASS │
└────┴───────────┴───────────┴─────┴─────────┴────────┘
(… 42 vetores no total)
┌─ sanity result ─┐
│ 42/42 passed │
└─────────────────┘
Rodar as suítes de testes
São duas, independentes. A do C++/CUDA roda pelo CTest, da raiz do projeto — são os 13 drivers de conformidade, cada um cruzando kernel × core × gold vector:
ctest --test-dir build --output-on-failure
A do frontend Python é independente da placa — só unittest da stdlib, sem venv e sem DLL.
De python/:
python -m unittest discover -s tests -v
São 1425 testes (~12 s); os que exigem uma DLL pulam sozinhos quando ela não foi compilada.
Olhe a CONTAGEM, não só o
OK. Um arquivo de teste que falha no import é reportado como um erro enquanto todos os testes dele somem em silêncio. Já aconteceu duas vezes aqui (112 e 170 testes desaparecidos). Se o número vier abaixo de 1425, tem arquivo não carregando.
Esse único comando cobre os dois backends: os testes de paridade são gerados uma vez
por backend (KnownKeyRecovery_cuda, KnownKeyRecovery_cpu, ...) e cada um dirige a sua
própria DLL no mesmo processo. Não é preciso definir KEYHUNT_BACKEND — quem só tem um
backend compilado vê os do outro pularem, dizendo qual biblioteca falta.
Escolher a GPU (--gpu)
Num PC com mais de uma placa, --list-gpus mostra os índices e --gpu N escolhe em qual rodar.
Para usar duas ao mesmo tempo, rode duas instâncias em terminais separados — cada uma na sua:
python main.py --list-gpus # lista as GPUs e seus índices
python main.py --preset wallet-71 --gpu 0 # 1a instância -> GPU 0
python main.py --preset wallet-71 --gpu 1 # 2a instância -> GPU 1 (outro terminal)
⚠️ Duas instâncias no mesmo alvo precisam de
--progress-suffix(logo abaixo), senão uma sobrescreve o arquivo de progresso da outra.
Duas instâncias no mesmo alvo (--progress-suffix)
O progresso fica em data/progress/<hash160>.json e é reescrito por inteiro a cada gravação,
a partir da memória do processo. Então duas instâncias no mesmo alvo apagam o trabalho uma da
outra: a última a gravar vence. --progress-suffix NOME dá a cada uma o seu próprio arquivo:
python main.py --preset wallet-71 --gpu 0 --progress-suffix gpu0 # -> <hash160>.gpu0.json
python main.py --preset wallet-71 --gpu 1 --progress-suffix gpu1 # -> <hash160>.gpu1.json
python main.py --preset wallet-71 --backend cpu --progress-suffix cpu
# no fim, consolide tudo no arquivo principal:
python main.py --join data/progress/<hash160>.gpu0.json data/progress/<hash160>.gpu1.json
- Nada de perda de dados: cada processo só lê e escreve o arquivo dele. O
--joinsem--outgrava emdata/progress/<hash160>.jsone mescla o que já estiver lá — é idempotente, dá para rodar quantas vezes quiser. - Uma instância não enxerga a outra durante a corrida. O preço é retrabalho quando as
duas planejam a mesma janela, nunca uma chave pulada. Com
--rng(janelas sorteadas num intervalo de 2^70) a chance é desprezível; no modo sequencial, dê faixas disjuntas a cada uma com--from/--range-size. - Depois do
--join, os arquivos por instância continuam com o conteúdo antigo. Se reusar o mesmo sufixo, aquela instância recomeça do progresso dela, não do consolidado — o que é seguro (re-varre, não pula). Apague-os depois de consolidar se quiser evitar isso. - Sem a flag, nada muda: continua o
<hash160>.jsoncompartilhado de sempre. Aceita letras, dígitos,_e-(1–32 caracteres) — qualquer outra coisa é recusada, não corrigida em silêncio, porque o valor decide qual arquivo a execução lê. Vale como chave de preset ("progress_suffix": "gpu0") e aparece no painel de resumo e no cabeçalho do Telegram — é assim que se distinguem duas instâncias reportando pro mesmo chat.
Se o ganho é throughput, cheque o hardware primeiro: duas instâncias na mesma GPU não aceleram nada — o kernel já satura a placa. Isso vale para duas GPUs, GPU +
--backend cpu, ou duas máquinas.Para várias máquinas o caminho melhor agora é o servidor (próxima seção): ele divide o trabalho na hora, sem arquivos por instância e sem
--join.
Buscar pelo servidor (--server-url)
Com o keyhunt-server rodando, a busca pede cada janela ao
servidor em vez de planejá-la contra o arquivo de progresso local. O servidor é a fonte da
verdade do que já foi varrido e reserva (lease) cada janela para quem pediu, então N
máquinas nunca recebem a mesma chave — é o substituto do --progress-suffix + --join.
python main.py --server --host 0.0.0.0 --port 8000 # na máquina do servidor
python main.py -w 71 --server-url http://192.168.0.10:8000 -bs 1e10 -bc 100 --rng # em cada máquina
python main.py --preset wallet-71-remote # o mesmo, pronto (127.0.0.1:8000)
- Token: cada máquina tem um uuid no
tokens.jsondo servidor; ponha-o emKEYHUNT_TOKEN=<uuid>nopython/.env(veja o.env.example). Nunca é uma flag (a linha de comando é visível para qualquer processo) e só o sha256 dele viaja. Precisa dohttpx(python -m pip install httpx), usado só nesse modo. - As estratégias continuam as mesmas: sequencial,
--from,--rng,--chunks(com--chunk-start) e--chunk-mode sweepviram pedidos de lease.--rng-mode extendnão existe no servidor e é recusado se digitado (umextendherdado de preset virarandom);--progress-suffixé recusado — não há arquivo local nesse modo. - Confere antes de varrer: token, alvo e — com
--wallet— que o range do servidor seja exatamente o do puzzle. Qualquer divergência para tudo com uma linha (exit 2), antes de escanear uma chave sequer. Com--target, vale o range do servidor. - Só o verificado é reportado: durante a janela vão heartbeats com o prefixo verificado (a
cada 30 s; janelas curtas não mandam nenhum) e, no fim — terminou,
Ctrl-C,--time-limitou achou —, o close com o prefixo verificado e a flagfound. A chave privada nunca sai da máquina: vai para oresults.jsonlocal como sempre. - Falhas mandam MENOS, nunca mais: se o servidor recusar um relato como rápido demais para o
token, a CLI espera e, no limite, fecha com o que já estava confirmado; um lease que expirou fica
só com o que os heartbeats confirmaram. As chaves não confirmadas voltam para o pool — são
re-varridas, nunca puladas. Um servidor fora do ar por mais de ~30 s encerra a busca com uma
linha, mantendo tudo que já foi reportado (e sem disparar o
--shutdown). - Painéis: o resumo ganha a linha
Server, e a cobertura mostrada é a do servidor (de todas as instâncias). Para ter uma cópia local do progresso, use oexport_progress.py(COMANDOS.md §13). --client-id NOMEnomeia a instância no log do servidor (padrão:<host>-<backend><gpu>).--tunee--testignoram--server-url, como fazem com--telegram.
Desligar o PC no fim (--shutdown)
Para varreduras longas sem ninguém olhando: --shutdown N desliga a máquina N segundos
depois que a execução termina — mas só se ela terminar sozinha.
python main.py --preset wallet-71-biggest -bc 40 --shutdown 30
python main.py --preset wallet-71 -tl 8h --shutdown 60 # roda 8h e desliga
| Como a execução terminou | Desliga? |
|---|---|
O plano acabou (--range-count cumprido) |
✅ sim |
| O intervalo foi 100% varrido | ✅ sim |
--time-limit estourou |
✅ sim |
Você apertou Ctrl-C |
❌ não |
| A chave foi encontrada | ❌ não — essa é a hora de estar no console |
| Erro, ou nem chegou a começar | ❌ não |
É exatamente essa condicional que faz a flag valer mais do que um shutdown /s /t 0 na
linha seguinte de um .bat: aquele dispara de qualquer jeito, inclusive depois de um
Ctrl-C ou de um erro na largada.
O mínimo é 5 segundos — a contagem regressiva é a sua única chance de cancelar (
shutdown /ano Windows,shutdown -cfora dele). Um valor menor é recusado com uma mensagem, não arredondado em silêncio. Quando dispara, a linhawarn shutting down in 30s - run 'shutdown /a' to cancelaparece no console.O progresso já está no disco antes do disparo (o
flushdo final da varredura vem primeiro), então cancelar o desligamento não perde nada. Aceita também o formato de duração do--time-limit(--shutdown 1m30s), vale como chave de preset ("shutdown": 30), e funciona nos dois backends. Vale só para a busca:--tunee--testignoram a flag, como já fazem com--telegrame--time-limit.
Referência rápida de comandos
📚 A tabela abaixo é o resumo. Para todas as formas de rodar o buscador — cada estratégia de varredura, presets, várias GPUs, execução sem supervisão, Telegram,
--tune, a API Python —, cada uma com uma explicação curta, veja a biblioteca de comandos (COMANDOS.md).
| Quero… | Comando |
|---|---|
| Buscar num puzzle | python main.py --wallet 71 --range-size 1e9 --rng |
| Buscar num endereço | python main.py --target 1BgGZ9… --from 0x1 --range-size 0x2000 |
| Buscar num puzzle a partir de uma chave exata | python main.py --wallet 71 --from 0x600000000000000000 -bs 1e9 |
| Usar um preset | python main.py --preset wallet-71 |
| Listar puzzles | python main.py --list-puzzles |
| Listar / escolher a GPU | python main.py --list-gpus · --gpu 1 |
| Duas instâncias no mesmo alvo | --gpu 0 --progress-suffix gpu0 / --gpu 1 --progress-suffix gpu1, depois --join |
| Várias máquinas sem colisão (servidor) | python main.py -w 71 --server-url http://192.168.0.10:8000 --rng -bs 1e10 |
| Amostrar o intervalo de forma espaçada | python main.py --wallet 71 --chunks 1000 -bs 1e9 |
| Amostrar a partir de uma faixa (0-based) | python main.py --wallet 71 --chunks 100 --chunk-start 90 -bs 1e9 |
| Varrer em sequência a partir de uma faixa | python main.py --wallet 71 --chunks 100 --chunk-start 90 --chunk-mode sweep -bs 1e9 |
| Achar a melhor config de GPU | python main.py --tune --wallet 71 |
| Rodar a noite toda e desligar | python main.py --preset wallet-71 -tl 8h --shutdown 30 |
| Testar o backend | python main.py --test |
| Rodar a suíte Python | python -m unittest discover -s tests -v |
| Rodar a suíte C++/CUDA | ctest --test-dir build --output-on-failure (da raiz) |
🧩 Sem GPU? Modo CPU (funciona sem NVIDIA/CUDA)
O projeto roda mesmo sem GPU NVIDIA e sem o CUDA Toolkit. Existe um segundo backend, escrito
em C++ puro (src/search/cpu/), que reaproveita exatamente os mesmos cores matemáticos (via o
caminho host que já era conferido contra os gold vectors) e expõe a mesma ABI C. Ou seja:
toda a CLI — progresso por alvo, retomada, --join, --time-limit, presets, monitor Telegram —
funciona sem alteração; só o motor por baixo muda.
-
Build: o CMake detecta o CUDA automaticamente. Com CUDA presente, compila os dois backends (
keyhunt_c.dll+keyhunt_cpu.dll); sem CUDA, configura como projeto C++ puro e compila só o backend CPU (keyhunt_cpu.dll+ os labs de CPU). Nenhuma edição necessária. -
Escolher o backend:
--backend {auto,cpu,cuda}ou a variável de ambienteKEYHUNT_BACKEND, nesta ordem de precedência:--backend> preset >KEYHUNT_BACKEND>autoO padrão
autousa a GPU se houver uma com CUDA, senão cai para a CPU. ExportarKEYHUNT_BACKENDvale para todos os comandos (o run, o--list-gpus, o--test); uma flag ou um preset sobrepõem a variável, mas nada a sobrescreve por conta própria.
python main.py --list-gpus # na CPU, mostra "CPU (<modelo>, N threads)"
python main.py --backend cpu --test # suíte de sanidade no backend CPU (42/42)
python main.py --backend cpu --preset puzzle-1-demo
$env:KEYHUNT_BACKEND = "cpu"; python main.py --preset puzzle-1-demo # idem, pela variável
Expectativa honesta: a CPU é ~100× mais lenta que a RTX 4060 — medido aqui: 10,7 Mk/s num Ryzen 7 5800X (16 threads) contra ~1.250 Mk/s na 4060; em CPUs mais modestas a distância passa de 1000×. A GPU vence pelo paralelismo massivo (dezenas de milhares de lanes); a CPU tem núcleos × threads. O backend CPU usa
std::thread+ a mesma inversão de Montgomery em lote — é "tão otimizado quanto um bom código de CPU", não uma throughput parecida com a da GPU. Mesma disciplina de correção (gold vectors + oráculo) e mesmo escopo (só chaves conhecidas).⚠️ Esses números valem para um build Release, que é o padrão desde que o
CMakeLists.txtpassou a defini-lo. Um build Debug custa ~6–8× no backend CPU (o código de device do CUDA é pouco afetado). O configure imprimecuda-labs: build type = ...— confira se algo parecer lento.
📈 Performance
A busca saiu de ~60 Mkeys/s na primeira versão para ~1.250 Mkeys/s (1,25 Gk/s — mais de
um bilhão de chaves por segundo) — cerca de 21× mais rápido — ao longo de três ciclos de
otimização guiados por profiler mais uma varredura conjunta com --tune, chegando ao mesmo nível
de referências como a VanitySearch.
O número vale na geometria tunada
lanes=49152 stride=8192 gpu_batch=768(a dos presetswallet-71*). A configuração anterior,32768/2048/384, entregava ~1.050 Mk/s: era um ótimo local, achado varrendo um botão de cada vez. Foi a varredura conjunta dos três que encontrou o ponto melhor.
Cada ganho foi medido, não adivinhado — várias ideias "óbvias" na verdade deixaram o kernel mais lento e foram descartadas. Os três saltos que ficaram:
| Otimização | O que muda | Ganho |
|---|---|---|
| Incremento afim + inversão em lote | troca a soma de pontos cara por +G barato, amortizando a inversão entre milhares de chaves |
~1,4× |
| Coalescência de memória (SoA) | acessos à memória alinhados, sem desperdício de banda | ~2,4× |
| Simetria ±jG | uma inversão gera dois pontos, dando ao compilador trabalho paralelo para esconder latência | ~1,6× |
As RTX 4060 tem limite de 115 W, então o clock e a velocidade oscilam com a temperatura — por isso o
--tunemede em janela quente com mediana. Números aqui são da RTX 4060.
🔍 Como sei que está correto
Um erro de sinal ou de endianness na matemática da curva não trava o programa — ele só gera silenciosamente um endereço errado. Comparar GPU contra CPU não pega isso, porque as duas rodam exatamente o mesmo código.
A rede de segurança é um oráculo independente em Python (tools/secp256k1_oracle.py,
big-ints puros + hashlib) que calcula os valores esperados por um caminho totalmente
diferente. Esses valores viram gold vectors embutidos em cada teste, e toda camada é conferida
contra eles. O marco de confiança: hash160(1) = 751e76e8…, que é exatamente o HASH160 público
do puzzle #1 — um número que dá para verificar em qualquer lugar.
Os gold vectors são versionados junto com o código; o build em CUDA nunca roda Python. Só é preciso regerar se você mexer no oráculo:
python tools\gen_vectors.py
🔒 Escopo & ética
Este é um exercício de aprendizado e correção — não uma ferramenta para atacar carteiras.
- Todos os testes ponta a ponta buscam apenas chaves que já conhecemos: puzzles que a gente
mesmo gera (escolhe
k, calcula o endereço, confirma que o kernel reencontrak) e o puzzle público #1 (chave1, sem saldo). - O espaço de 256 bits não é força-brutável — é a própria base da segurança do Bitcoin. Nada aqui mira, ou poderia mirar, endereços com fundos.
- As referências (BitCrack, VanitySearch, Keyhunt) entram só como comparação de arquitetura.
🗂️ Estrutura do projeto
cuda-labs/
├── src/ todo o C++/CUDA — é também o ÚNICO include root
│ ├── common/ portability/cuda_compat.h (shim host) + host/ + cuda/ (CUDA_CHECK, benchmark)
│ ├── hashing/ algoritmos de hash (SHA-256, RIPEMD-160, HASH160) → hashing (CUDA)
│ ├── secp256k1/ u256 → corpo → curva → escalar → batchinv → pubkey → addr → secp256k1 (CUDA)
│ ├── search/ os dois backends de busca, lado a lado
│ │ ├── cuda/ kernel fundido tunado (~1.250 Mk/s) → search_cuda (CUDA)
│ │ └── cpu/ varredura em threads, C++ puro, sem CUDA → search_cpu
│ └── capi/ ABI C (extern "C"): keyhunt_c.dll + keyhunt_cpu.dll, mesma ABI
├── tests/ drivers de conformidade — cruzam GPU × CPU × gold; vectors/ tem os gold
├── python/ frontend: pykeyhunt (ctypes, escolhe o backend) + keyhunt_cli + testes
│ ├── shared/ código comum ao CLI e ao servidor (janelas, Base58/WIF, tokens)
│ └── keyhunt-server/ servidor de ranges (FastAPI + SQLite): `python main.py --server`;
│ a busca o usa com `--server-url URL`
└── tools/ oráculo de referência em Python + gerador de gold vectors
Um único include root. Todo include interno é escrito qualificado a partir de
src/—#include "secp256k1/field/field.cuh",#include "common/portability/cuda_compat.h"— então a linha do include já diz onde a dependência mora, sem include-path por diretório.
Os cores em
secp256k1//hashing/são__host__ __device__e compilam nos dois backends: sob onvcc(GPU) e como C++ puro (CPU, viasrc/common/portability/cuda_compat.h, que anula os qualificadores CUDA no host).keyhunt_c.dllekeyhunt_cpu.dllexportam a mesma ABI.
Os drivers de conformidade ficam em build/tests/ — cada um imprime PASS/FAIL por vetor e
sai com código 0 (passou) ou 1 (falhou). Estão registrados no CTest, então rodar todos é:
ctest --test-dir build --output-on-failure
Um driver isolado continua funcionando como executável (.\build\tests\test-keyhunt.exe), e
ctest -R field roda só os que casam com o nome.
🔧 Aprofundando (para devs)
Versões da toolchain (e por quê)
- CUDA 13.3 é o toolkit no PATH. O 13.0, também instalado, rejeita o compilador do VS 2026 (MSVC 19.50) — não use.
- Visual Studio 2026 fornece o
cl.exeque onvccusa como host compiler. Ele não fica no PATH de um PowerShell comum; daí oLaunch-VsDevShell.ps1do Começando rápido. Para confirmar:Get-Command cldeve apontar para…\MSVC\14.50.…\bin\HostX64\x64\cl.exe. - O
Launch-VsDevShell.ps1pode imprimir um avisovswhere.exe não reconhecido— pode ignorar.
Arquitetura de GPU — native (um build roda em qualquer placa da máquina)
O build usa CMAKE_CUDA_ARCHITECTURES=native: detecta todas as GPUs instaladas e gera um
cubin nativo por arquitetura no mesmo binário (ex.: RTX 4060 sm_89 + RTX 50xx sm_120 lado a
lado). É cubin puro, sem PTX embutido — necessário porque o driver instalado é mais antigo
que o toolkit 13.3 e rejeitaria PTX em tempo de execução ("the provided PTX was compiled with an
unsupported toolchain"). Trocou/adicionou uma GPU? Basta recompilar — sem editar a arquitetura.
CMAKE_CUDA_ARCHITECTURESé cache: umbuild/já existente mantém a arquitetura da primeira configuração. Para onativereavaliar (ex.: depois de plugar outra placa), apaguebuild/CMakeCache.txtou configure num diretório novo.
Integração Python (a DLL e a API)
A DLL keyhunt_c.dll (CUDA) faz parte do build normal (cmake --build build já a gera em
build/capi/, junto com keyhunt_cpu.dll). O lado Python é puro ctypes, sem compilar nada. A
DLL CUDA depende do cudart, que o loader (python/pykeyhunt/_ffi.py)
encontra via CUDA_PATH\bin. O loader escolhe o backend por KEYHUNT_BACKEND
(auto/cuda/cpu; padrão auto = GPU se houver, senão CPU) — veja pykeyhunt.backend().
KEYHUNT_DLL aponta uma biblioteca específica e ganha de tudo. A variável é lida uma vez, no
import: para trocar de backend dentro do mesmo processo, importe o módulo sob outro nome.
Além da CLI, dá para usar a API diretamente:
from pykeyhunt import Search
with Search("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH", base_key=1, count=1 << 12) as s:
for p in s: # cada iteração avança a busca uma "wave"
print(f"{p.fraction:6.1%} {p.rate_kps/1e6:5.1f} Mkeys/s")
if p.found:
print("chave privada:", p.found_key_int) # -> 1
break
A suíte de testes cobre o binding e todo o frontend -- de python/, rode
python -m unittest discover -s tests -v. São 1425 testes (~12 s): os unitários rodam em qualquer
máquina, e os de integração rodam contra cada backend compilado (pulando os das DLLs ausentes).
Adicionar um novo lab / algoritmo
Código de algoritmo mora numa lib sob src/ (src/hashing/ para hashes, src/secp256k1/ para
a curva, src/search/ para os backends de busca); tests/ só tem drivers.
- Core em
src/secp256k1/<nome>/<nome>.cuh— um core inline__host__ __device__(para inlinar no kernel; sem-rdc). Kernels + launchers vão no<nome>.cu, que entra noCMakeLists.txtda lib. - Gold — adicione um
gen_<nome>()emtools/gen_vectors.py, rode o gerador e versione otests/vectors/gold_<nome>.h. - Driver
tests/<área>/<nome>/main.cu— cruza kernel × core × gold e retorna ≠0 em falha. Registre comadd_conformance_test(test-<nome> <área>/<nome>/main.cu <libs...>)notests/CMakeLists.txt; isso já cria o executável embuild/tests/e o inscreve no CTest.
Includes. Escreva sempre qualificado a partir de
src/(#include "secp256k1/field/field.cuh"); o gold é#include "vectors/gold_<nome>.h".
Por que o core fica no header? Para o compilador inlinar o algoritmo dentro do kernel — é isso que garante o throughput. Um split clássico exigiria separable compilation (
-rdc) e não inlinaria entre unidades de tradução.
A documentação interna detalhada (decisões, gotchas, histórico de otimização) fica em
.agents/.
Build limpo
O diretório build/ é descartável e ignorado pelo git:
Remove-Item -Recurse -Force build
cmake -B build -G Ninja ; if ($?) { cmake --build build }
Projeto pessoal de estudo de CUDA. Referências de arquitetura: BitCrack, VanitySearch, Keyhunt. Sem afiliação; sem uso contra endereços com fundos.