diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..d995617
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+bin/
+.git/
+dist/
+packaging/
+docs/*.gif
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5c854ce..78c9a37 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,3 +26,4 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 1c3d37c..43fc271 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -48,6 +48,26 @@ nfpms:
- deb
- rpm
+# Publishes to github.com/bytestrix/homebrew-tap. Skipped until that repo
+# exists and a HOMEBREW_TAP_TOKEN secret (a PAT with write access to it) is set.
+homebrew_casks:
+ - name: vanityrig
+ ids: [vanityrig]
+ homepage: https://github.com/bytestrix/vanityrig
+ description: Vanity Tor v3 .onion address finder with prefix/suffix/anywhere matching.
+ license: MIT
+ repository:
+ owner: bytestrix
+ name: homebrew-tap
+ token: '{{ envOrDefault "HOMEBREW_TAP_TOKEN" "" }}'
+ skip_upload: '{{ if eq (envOrDefault "HOMEBREW_TAP_TOKEN" "") "" }}true{{ else }}false{{ end }}'
+ hooks:
+ post:
+ install: |
+ if OS.mac?
+ system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/vanityrig"]
+ end
+
release:
github:
owner: bytestrix
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ee9af30
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM golang:1.26-alpine AS build
+WORKDIR /src
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /vanityrig ./cmd/vanityrig
+
+FROM alpine:3
+COPY --from=build /vanityrig /usr/local/bin/vanityrig
+ENTRYPOINT ["vanityrig"]
diff --git a/Makefile b/Makefile
index 5fe9e28..a0d8fe8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build test lint
+.PHONY: build test lint demo
build:
go build -o bin/vanityrig ./cmd/vanityrig
@@ -8,3 +8,7 @@ test:
lint:
go vet ./...
+
+# Regenerates docs/demo.gif — needs vhs (https://github.com/charmbracelet/vhs).
+demo:
+ vhs docs/demo.tape
diff --git a/README.md b/README.md
index 5358b5a..7c1c788 100644
--- a/README.md
+++ b/README.md
@@ -13,39 +13,41 @@
Quick start ·
Usage ·
Why VanityRig ·
+ Comparison ·
Contributing ·
Security
---
-## What is it?
+**Find a Tor v3 `.onion` address containing your word — at the start, the
+end, or anywhere — and know exactly how long it will take before you spend
+any CPU on it.**
-A Tor v3 `.onion` address is 56 random-looking characters. VanityRig finds
-one containing a word you choose — at the start, at the end, or anywhere in
-the middle — and tells you honestly what that will cost **before** it spends
-any CPU time on it.
+
+
+
-It's built on [`mkp224o`](https://github.com/cathugger/mkp224o), the
-standard generator for this, and adds the three things that tool doesn't
-have: an accurate up-front time/cost estimate, a live progress dashboard, and
-the ability to match a word anywhere in the address rather than only at the
-start — which is often dramatically faster than people expect.
+```sh
+curl -fsSL https://raw.githubusercontent.com/bytestrix/vanityrig/main/install.sh | bash
+vanityrig
+```
---
## Quick start
-**1. Install**
+**1. Install** (Linux/macOS)
```sh
curl -fsSL https://raw.githubusercontent.com/bytestrix/vanityrig/main/install.sh | bash
```
-Downloads the right prebuilt binary for your OS/architecture from the
-[latest release](https://github.com/bytestrix/vanityrig/releases/latest) —
-no Go toolchain required. Linux and macOS only; Windows users, grab the
-`.zip` from the releases page directly.
+**Windows** (PowerShell):
+
+```powershell
+irm https://raw.githubusercontent.com/bytestrix/vanityrig/main/install.ps1 | iex
+```
**2. Run it**
@@ -53,25 +55,30 @@ no Go toolchain required. Linux and macOS only; Windows users, grab the
vanityrig
```
-That's it — no flags to learn. A real dashboard opens, with everything on one
-screen at once: **Configuration** (word, save location, match mode, CPU
-share), **Statistics** (live prefix/suffix/anywhere cost comparison,
-including difficulty as 2^N bits, until you start), **Progress**,
-**Resources** (CPU share and, where the kernel exposes one, real CPU
-temperature), **GPU Status** (honestly "coming soon" — there is no GPU engine
-here, so nothing here is a fabricated number), and a scrolling **Logs** panel
-with a cyclable level filter (`v`). Tab between fields, arrow keys to change a
-selection, `enter` to edit a field or start, `s` to start/stop/restart a
-search without quitting, `q` to quit — Configuration freezes in place while
-running and the rest switch to live search data, without ever leaving this
-screen.
-
-Already know what you want? Skip the prompts with `vanityrig [flags]`
-— see [Usage](#usage) below.
+No flags to learn. Type a word, press `s`, and watch it search.
+
+| Key | Action |
+|---|---|
+| `tab` / `↑↓` | move between fields |
+| `←→` | change a selection |
+| `enter` | edit a field |
+| `s` | start / stop the search |
+| `v` | cycle the log level filter |
+| `q` | quit |
+
+Already know what you want? `vanityrig [flags]` — see [Usage](#usage).
+
+Found an address? See [Using your address with Tor](docs/using-your-address.md).
Other ways to install
+**Homebrew (macOS/Linux):**
+
+```sh
+brew install --cask bytestrix/tap/vanityrig
+```
+
**Debian/Ubuntu (.deb) or Fedora/RHEL (.rpm):** download the package for
your architecture from the
[latest release](https://github.com/bytestrix/vanityrig/releases/latest)
@@ -88,6 +95,13 @@ sudo rpm -i vanityrig_*_linux_amd64.rpm # Fedora/RHEL
go install github.com/bytestrix/vanityrig/cmd/vanityrig@latest
```
+**Docker:**
+
+```sh
+docker build -t vanityrig https://github.com/bytestrix/vanityrig.git
+docker run --rm -it -v "$PWD/keys:/keys" vanityrig cafe -out /keys
+```
+
**Build from source:**
```sh
@@ -102,7 +116,7 @@ Optional, for any install method: put
uses it automatically for prefix searches — it's roughly 8x faster per core
than VanityRig's own engine for that mode (measured, not estimated — see
`internal/engine/bench_test.go`). Suffix and anywhere searches always run on
-VanityRig's built-in engine, since `mkp224o` can't do those at all.
+VanityRig's built-in engine.
@@ -161,8 +175,7 @@ input.
- **Matches anywhere in the address, not just the start.** A word has up to
47 possible positions in a 56-character address — matching any of them is
- often 10-50x faster than pinning it to the front. No other generator
- supports this.
+ often 10-50x faster than pinning it to the front.
- **Compares where the word can go, not just how long it takes.** Prefix,
suffix, and anywhere have very different costs for the same word, so
VanityRig checks all three and tells you which is achievable — instead of
@@ -186,6 +199,25 @@ input.
---
+## Comparison
+
+| | VanityRig | mkp224o |
+|---|---|---|
+| Prefix match | ✅ (uses mkp224o if installed) | ✅ |
+| Suffix match | ✅ | ❌ |
+| Match anywhere | ✅ | only via a custom `--enable-regex` build |
+| Time estimate before searching | ✅ | ❌ |
+| Detects impossible patterns | ✅ | ❌ |
+| Live dashboard | ✅ | ❌ |
+| Resume after stop | ✅ | ❌ |
+| Prefix speed per core | ~8x slower (built-in engine) | fastest |
+| GPU | not yet ([#2](https://github.com/bytestrix/vanityrig/issues/2)) | ❌ |
+
+VanityRig isn't trying to replace `mkp224o` — it drives it for prefix
+searches when it's on your `PATH`, and fills in what it doesn't do.
+
+---
+
## Contributing
Contributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the
diff --git a/docs/demo.gif b/docs/demo.gif
new file mode 100644
index 0000000..63a827e
Binary files /dev/null and b/docs/demo.gif differ
diff --git a/docs/demo.tape b/docs/demo.tape
new file mode 100644
index 0000000..2f1d862
--- /dev/null
+++ b/docs/demo.tape
@@ -0,0 +1,32 @@
+# Regenerate the README demo with: vhs docs/demo.tape
+Output docs/demo.gif
+
+Set Shell "bash"
+Set FontSize 14
+Set Width 1400
+Set Height 800
+Set Theme "Catppuccin Mocha"
+Set TypingSpeed 80ms
+
+Hide
+Type "make build >/dev/null && export PATH=$PWD/bin:$PATH && clear"
+Enter
+Show
+
+Type "vanityrig"
+Sleep 400ms
+Enter
+Sleep 1.5s
+
+# Edit the word field and type a word
+Enter
+Type "cafe"
+Enter
+Sleep 2s
+
+# Start the search and let it find matches
+Type "s"
+Sleep 8s
+
+Type "q"
+Sleep 500ms
diff --git a/docs/using-your-address.md b/docs/using-your-address.md
new file mode 100644
index 0000000..a544404
--- /dev/null
+++ b/docs/using-your-address.md
@@ -0,0 +1,53 @@
+# Using your address with Tor
+
+Every match VanityRig finds is saved as its own directory inside the output
+folder (default `~/.vanityrig/keys`), named after the address:
+
+```
+~/.vanityrig/keys/cafexyz…abcd.onion/
+├── hostname # the full .onion address
+├── hs_ed25519_public_key
+└── hs_ed25519_secret_key # keep this private — whoever has it owns the address
+```
+
+These are exactly the files Tor expects in a hidden service directory.
+
+## 1. Copy the directory to where Tor can read it
+
+```sh
+sudo mkdir -p /var/lib/tor/my_service
+sudo cp ~/.vanityrig/keys/.onion/* /var/lib/tor/my_service/
+sudo chown -R debian-tor:debian-tor /var/lib/tor/my_service # user is "tor" on Arch/Fedora
+sudo chmod 700 /var/lib/tor/my_service
+sudo chmod 600 /var/lib/tor/my_service/*
+```
+
+Tor refuses to start if the directory permissions are too open.
+
+## 2. Point Tor at it
+
+Add to `/etc/tor/torrc`:
+
+```
+HiddenServiceDir /var/lib/tor/my_service/
+HiddenServicePort 80 127.0.0.1:8080
+```
+
+This forwards port 80 of your `.onion` to a web server on local port 8080.
+
+## 3. Restart Tor and check
+
+```sh
+sudo systemctl restart tor
+sudo cat /var/lib/tor/my_service/hostname
+```
+
+The hostname printed must be the address VanityRig found. Open it in Tor
+Browser to confirm the service is reachable.
+
+## Keep the secret key safe
+
+- Back up `hs_ed25519_secret_key` somewhere offline — if you lose it, the
+ address is gone for good.
+- Anyone with a copy can impersonate your service. Delete the matches you
+ aren't going to use.
diff --git a/install.ps1 b/install.ps1
new file mode 100644
index 0000000..d60d95e
--- /dev/null
+++ b/install.ps1
@@ -0,0 +1,41 @@
+# Installs the latest vanityrig release binary on Windows.
+# Usage: irm https://raw.githubusercontent.com/bytestrix/vanityrig/main/install.ps1 | iex
+$ErrorActionPreference = 'Stop'
+
+$repo = 'bytestrix/vanityrig'
+$binDir = if ($env:VANITYRIG_INSTALL_DIR) { $env:VANITYRIG_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'vanityrig' }
+
+$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
+ 'AMD64' { 'amd64' }
+ 'ARM64' { 'arm64' }
+ default { throw "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
+}
+
+$version = (Invoke-RestMethod "https://api.github.com/repos/$repo/releases/latest").tag_name
+if (-not $version) { throw "could not determine the latest release of $repo" }
+
+$archive = "vanityrig_windows_$arch.zip"
+$url = "https://github.com/$repo/releases/download/$version/$archive"
+
+$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid())
+New-Item -ItemType Directory -Path $tmp | Out-Null
+try {
+ Write-Host "Downloading vanityrig $version for windows/$arch..."
+ Invoke-WebRequest $url -OutFile (Join-Path $tmp $archive)
+ Expand-Archive (Join-Path $tmp $archive) -DestinationPath $tmp
+
+ New-Item -ItemType Directory -Force -Path $binDir | Out-Null
+ Move-Item -Force (Join-Path $tmp 'vanityrig.exe') (Join-Path $binDir 'vanityrig.exe')
+} finally {
+ Remove-Item -Recurse -Force $tmp
+}
+
+Write-Host "Installed vanityrig $version to $binDir\vanityrig.exe"
+
+$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
+if (($userPath -split ';') -notcontains $binDir) {
+ [Environment]::SetEnvironmentVariable('Path', "$userPath;$binDir", 'User')
+ Write-Host "Added $binDir to your PATH. Open a new terminal, then run: vanityrig"
+} else {
+ Write-Host 'Run it: vanityrig'
+}
diff --git a/install.sh b/install.sh
index e482313..b0bedfe 100755
--- a/install.sh
+++ b/install.sh
@@ -19,7 +19,7 @@ case "$os" in
linux|darwin) ;;
*)
echo "error: this script supports Linux and macOS only." >&2
- echo "Windows: download a .zip from https://github.com/$REPO/releases/latest" >&2
+ echo "Windows: irm https://raw.githubusercontent.com/$REPO/main/install.ps1 | iex" >&2
exit 1
;;
esac
diff --git a/internal/tui/setup_test.go b/internal/tui/setup_test.go
index 9ead1dd..4fe96f5 100644
--- a/internal/tui/setup_test.go
+++ b/internal/tui/setup_test.go
@@ -30,6 +30,18 @@ func key(s string) tea.KeyMsg {
}
}
+// stopAndWait cancels a running search and waits for the runner to finish
+// writing its state, so t.TempDir cleanup never races a final save.
+func stopAndWait(t *testing.T, s *Setup) {
+ t.Helper()
+ s.cancel()
+ select {
+ case <-s.run.Done():
+ case <-time.After(10 * time.Second):
+ t.Fatal("runner did not stop within 10s of cancel")
+ }
+}
+
func press(s *Setup, keys ...string) {
for _, k := range keys {
s.Update(key(k))
@@ -266,7 +278,7 @@ func TestNoLineExceedsTerminalWidth(t *testing.T) {
}
}
if running {
- s.cancel()
+ stopAndWait(t, s)
}
}
}
@@ -382,7 +394,7 @@ func TestMouseIgnoredOnceRunning(t *testing.T) {
if s.mode != before {
t.Error("clicking after the search has started must not change settings")
}
- s.cancel()
+ stopAndWait(t, s)
}
// The default log level (info) hides routine STATS throughput lines but
@@ -680,5 +692,5 @@ func TestDashboardNeverShowsFabricatedData(t *testing.T) {
t.Errorf("dashboard must never show fabricated data %q:\n%s", f, out)
}
}
- s.cancel()
+ stopAndWait(t, s)
}