Skip to content

Repository files navigation

vanity-fleet — grind a Solana vanity keypair on an AWS spot GPU fleet

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.

Why it's built this way

  • Capacity. A single large GPU instance often fails with InsufficientInstanceCapacity. An EC2 Spot Fleet (price-capacity-optimized) draws gpu_target GPUs from many instance sizes across every AZ, and type=maintain auto-replaces interruptions. make capacity ranks 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. vanity is 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.

Prereqs

  • tofu, packer, aws (v2), jq, solana-keygen, ssh-keygen, make on PATH.
  • AWS credentials for the target account (aws sso login / AWS_PROFILE=…).
  • Spot vCPU quota for G instances in the target region (make capacity prints it).
  • Optional but recommended: vanity installed locally (cargo install vanity --features=gpu) so you can benchmark your pattern before paying for a fleet. See Sizing.

Run it

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 hit

make 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.

Pattern: prefix, suffix, count

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.

Spend ceiling

The poller enforces a hard ceiling when you pass an hourly estimate:

FLEET_HOURLY=22 SPEND_CEILING=400 make poll   # abort+alert past ~$400

Estimate FLEET_HOURLYgpu_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.

Sizing: benchmark first, then divide

Do not size from a table. Run the grinder for 60 seconds, locally or on one node:

vanity grind-keypair --prefix <yours> --num-gpus 1

It 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 19, AH, or J followed by 19/AD. 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 poll

If cps scales with vCPUs on your fleet, prefer larger instance sizes per GPU.

Files

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.

Verify

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 resources

Security

Mitigations 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.

License

MIT. See LICENSE. The grinder itself is a separate project with its own license: cavemanloverboy/vanity.

About

Grind a Solana vanity keypair on an AWS spot GPU fleet (OpenTofu + Packer). Zero-egress nodes, secret pulled over SSH only.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages