Declarative create → grind → teardown with OpenTofu + Packer. Finds an ed25519 keypair whose base58 pubkey starts and/or ends with a string you choose, using a spot GPU fleet, and pulls the secret to your machine only, over SSH, never to cloud storage. The GPU grinder is cavemanloverboy/vanity; this repo is only the orchestration around it.
- Capacity. A single large GPU instance often fails with
InsufficientInstanceCapacity. An EC2 Spot Fleet (price-capacity-optimized) drawsgpu_targetGPUs from many instance sizes across every AZ, andtype=maintainauto-replaces interruptions.make capacityranks region × type by live pool depth and price before you launch. - Race semantics (N searchers, first hit wins). Grinding runs async via instance user-data; a re-runnable poller pulls each hit and tears the fleet down when done.
- Security.
vanityis prebaked into a private AMI so the grind fleet runs with egress fully denied: no channel to exfiltrate the key. The secret lives only in/dev/shm(tmpfs) on the instance and is pulled via inbound SSH, then verified locally. See SECURITY.md for the irreducible residual risk.
tofu,packer,aws(v2),jq,solana-keygen,ssh-keygen,makeon PATH.- AWS credentials for the target account (
aws sso login/AWS_PROFILE=…). - Spot vCPU quota for G instances in the target region (
make capacityprints it). - Optional but recommended:
vanityinstalled locally (cargo install vanity --features=gpu) so you can benchmark your pattern before paying for a fleet. See Sizing.
make key # one-time: operator SSH keypair -> vanity-fleet-key.pem/.pub
make myip # -> prints the ssh_cidr line for your tfvars
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # set region, ssh_cidr, prefix/suffix, gpu_target, instance_types
make capacity # rank region x type by pool depth + $/GPU-hr
# -> pick the sweet spot, update terraform.tfvars
make bake # one-time per region: build the vanity AMI -> ami.auto.tfvars
make grind # apply fleet -> poll -> on verified hit(s), auto-clean
# writes keys/<pubkey>.json (mode 600) per hitmake grind blocks while polling. If your laptop sleeps or the network drops, re-run
make poll: it resumes against the live fleet and its key tally from already-saved
files. make clean (idempotent) destroys everything and runs a cross-region zombie
sweep. make status shows per-node health and throughput.
The example tfvars grinds the prefix Demo, which finishes in seconds on one GPU and
exercises the whole pipeline (bake → fleet → poll → SSH pull → verify → teardown) for
about a dollar. Do that once before a real run.
Set any of these in terraform.tfvars (at least one of prefix/suffix is required).
Base58 excludes 0, O, I and l, so those characters can never appear.
prefix = "Demo" # pubkey must START with this (base58, case-sensitive)
suffix = "xyz" # pubkey must END with this (optional)
num_keys = 3 # grind until 3 matching keys are found, then stop
case_insensitive = false # true = ignore case (much easier/faster)Each node grinds with --count num_keys so no node idles after its first find; the
poller collects unique keys across the whole fleet, saves each as keys/<pubkey>.json,
and tears down once num_keys are verified. Difficulty multiplies with prefix+suffix
length; a suffix on top of a long prefix is astronomically harder. Size accordingly.
The poller enforces a hard ceiling when you pass an hourly estimate:
FLEET_HOURLY=22 SPEND_CEILING=400 make poll # abort+alert past ~$400Estimate FLEET_HOURLY ≈ gpu_target × $/GPU-hr from make capacity output.
Default (FLEET_HOURLY=0) disables the guard. Independently, make grind sets an
AWS-side valid_until deadline (MAX_HOURS, default 24) after which the fleet
terminates itself even if your credentials have expired.
Do not size from a table. Run the grinder for 60 seconds, locally or on one node:
vanity grind-keypair --prefix <yours> --num-gpus 1It prints expected: N attempts and a live attempts/sec. Then:
mean GPU-hours = N / (attempts_per_sec_per_GPU × 3600)
mean wall-clock = GPU-hours / gpu_target
cost ≈ GPU-hours × $/GPU-hr (from `make capacity`)
Search time is exponential, so plan for the tail: the 95th percentile is about 3× the mean. Cost is roughly constant in fleet size; a bigger fleet buys wall-clock, not savings.
Base58 leading-character trap. A 32-byte key encodes to 43 characters (~6% of keys)
or 44 characters (~94%). A 44-character address can only begin with 1–9, A–H,
or J followed by 1–9/A–D. Any prefix outside that set, which includes every
prefix starting with a lowercase letter and everything from K onward, can only match
the rare 43-character addresses and is about 17× harder than the naive 58ⁿ estimate.
Demo costs the naive rate; Vanity costs 17 × 58⁶. The grinder's printed expected
value already accounts for this, so trust that number and not your own 58ⁿ arithmetic.
Throughput is hardware- and host-dependent. The poller's ETA line uses
RATE_PER_GPU (default 48M keys/s, one L40S on a 4-vCPU g6e.xlarge). That figure may
be host-bottlenecked; workstation cards of the same generation have been measured well
above 500M keys/s. Read the real cps from make status and override for a truthful
ETA:
RATE_PER_GPU=600000000 make pollIf cps scales with vCPUs on your fleet, prefer larger instance sizes per GPU.
| file | role |
|---|---|
main.tf |
provider, default-VPC data, key pair, egress-denied SG, launch template, EC2 spot fleet |
variables.tf / outputs.tf |
inputs (project_name, region, prefix, gpu_target, types, ssh_cidr…) / values the scripts read back |
userdata.sh.tftpl |
boot-time grind → /dev/shm/grind.out + grind.status (tmpfs only) |
terraform.tfvars.example |
starting point for your terraform.tfvars |
bake/vanity.pkr.hcl |
Packer build of the vanity AMI (cuda_arch default 89 = L40S / RTX Ada) |
bin/common.sh |
shared script settings: project name, key path, output dir, tag filter |
bin/capacity.sh |
sweet-spot finder (placement scores + spot price + quota) |
bin/poll.sh |
watch fleet, pull + verify hits into keys/, spend-ceiling guard, ETA |
bin/status.sh |
per-node grind status, cps, GPU utilization |
bin/sweep.sh |
cross-region check for leftover billing resources |
Makefile |
key · myip · capacity · bake · grind · poll · status · clean · sweep · check |
All resources are tagged Project=<project_name> (default vanity-fleet); the scripts
discover nodes by that tag, so several fleets can coexist under different names.
The poller only saves a keypair after solana-keygen pubkey reproduces an address that
matches your pattern. Confirm yourself:
solana-keygen pubkey keys/<pubkey>.json # must print the matching address
make sweep # must report zero live resourcesMitigations applied: zero-egress SG, tmpfs-only secret, SSH-pull only, SG locked to your
/32, self-built AMI from the official AWS Deep Learning base + OSS vanity, AWS-side
kill switch. What remains is host and hypervisor trust in AWS, and trust in everyone
with access to your AWS account. Ed25519 has no practical split-key grind, so the node
that finds the key holds the full secret in memory, on rented hardware. Details in
SECURITY.md.
Never commit anything under keys/, or any *.pem / *.pub / *.tfstate* /
terraform.tfvars. .gitignore covers them; check git status before pushing anyway.
MIT. See LICENSE. The grinder itself is a separate project with its own license: cavemanloverboy/vanity.