main.go raw
1 // Package main is a simple nostr key miner that uses the fast bitcoin secp256k1
2 // C library to derive npubs with specified prefix/infix/suffix strings present.
3 package main
4
5 import (
6 "bytes"
7 "encoding/hex"
8 "fmt"
9 "os"
10 "runtime"
11 "strings"
12 "sync"
13 "sync/atomic"
14 "time"
15
16 "next.orly.dev/pkg/lol/chk"
17 "next.orly.dev/pkg/lol/log"
18
19 "next.orly.dev/pkg/nostr/crypto/ec/bech32"
20 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
21 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
22
23 "github.com/alexflint/go-arg"
24 )
25
26 var prefix = append(bech32encoding.PubHRP, '1')
27
28 const (
29 PositionBeginning = iota
30 PositionContains
31 PositionEnding
32 )
33
34 type Result struct {
35 sec []byte
36 npub []byte
37 pub []byte
38 }
39
40 var args struct {
41 String string `arg:"positional" help:"the string you want to appear in the npub"`
42 Position string `arg:"positional" default:"end" help:"[begin|contain|end] default: end"`
43 Threads int `help:"number of threads to mine with - defaults to using all CPU threads available"`
44 }
45
46 func main() {
47 arg.MustParse(&args)
48 if args.String == "" {
49 _, _ = fmt.Fprintln(
50 os.Stderr,
51 `Usage: vainstr [--threads THREADS] [STRING [POSITION]]
52
53 Positional arguments:
54 STRING the string you want to appear in the npub
55 POSITION [begin|contain|end] default: end
56
57 Options:
58 --threads THREADS number of threads to mine with - defaults to using all CPU threads available
59 --help, -h display this help and exit`,
60 )
61 os.Exit(0)
62 }
63 var where int
64 canonical := strings.ToLower(args.Position)
65 switch {
66 case strings.HasPrefix(canonical, "begin"):
67 where = PositionBeginning
68 case strings.Contains(canonical, "contain"):
69 where = PositionContains
70 case strings.HasSuffix(canonical, "end"):
71 where = PositionEnding
72 }
73 if args.Threads == 0 {
74 args.Threads = runtime.NumCPU()
75 }
76 if err := Vanity(args.String, where, args.Threads); chk.E(err) {
77 log.F.F("error: %s", err)
78 }
79 }
80
81 func Vanity(str string, where int, threads int) (err error) {
82 // check the string has valid bech32 ciphers
83 for i := range str {
84 wrong := true
85 for j := range bech32.Charset {
86 if str[i] == bech32.Charset[j] {
87 wrong = false
88 break
89 }
90 }
91 if wrong {
92 return fmt.Errorf(
93 "found invalid character '%c' only ones from '%s' allowed\n",
94 str[i], bech32.Charset,
95 )
96 }
97 }
98 started := time.Now()
99 quit := make(chan struct{})
100 resC := make(chan Result)
101
102 // Handle interrupt
103 go func() {
104 c := make(chan os.Signal, 1)
105 <-c
106 close(quit)
107 log.I.Ln("\rinterrupt signal received")
108 os.Exit(0)
109 }()
110
111 var wg sync.WaitGroup
112 var counter int64
113 for i := 0; i < threads; i++ {
114 log.D.F("starting up worker %d", i)
115 go mine(str, where, quit, resC, &wg, &counter)
116 }
117 tick := time.NewTicker(time.Second * 5)
118 var res Result
119 out:
120 for {
121 select {
122 case <-tick.C:
123 workingFor := time.Now().Sub(started)
124 wm := workingFor % time.Second
125 workingFor -= wm
126 fmt.Printf(
127 " working for %v, attempts %d",
128 workingFor, atomic.LoadInt64(&counter),
129 )
130 case r := <-resC:
131 // one of the workers found the solution
132 res = r
133 // tell the others to stop
134 close(quit)
135 break out
136 }
137 }
138
139 // wait for all workers to stop
140 wg.Wait()
141
142 fmt.Printf(
143 "\r# generated in %d attempts using %d threads, taking %v ",
144 atomic.LoadInt64(&counter), args.Threads, time.Now().Sub(started),
145 )
146 fmt.Printf(
147 "\nHSEC = %s\nHPUB = %s\n",
148 hex.EncodeToString(res.sec),
149 hex.EncodeToString(res.pub),
150 )
151 nsec, _ := bech32encoding.BinToNsec(res.sec)
152 fmt.Printf("NSEC = %s\nNPUB = %s\n", nsec, res.npub)
153 return
154 }
155
156 func mine(
157 str string, where int, quit <-chan struct{}, resC chan Result, wg *sync.WaitGroup,
158 counter *int64,
159 ) {
160 wg.Add(1)
161 defer wg.Done()
162
163 var r Result
164 var e error
165 found := false
166 out:
167 for {
168 select {
169 case <-quit:
170 if found {
171 // send back the result
172 log.D.Ln("sending back result")
173 resC <- r
174 log.D.Ln("sent")
175 } else {
176 log.D.Ln("other thread found it")
177 }
178 break out
179 default:
180 }
181 atomic.AddInt64(counter, 1)
182 r.sec, r.pub, e = Gen()
183 if e != nil {
184 log.E.Ln("error generating key: '%v' worker stopping", e)
185 break out
186 }
187 if r.npub, e = bech32encoding.BinToNpub(r.pub); e != nil {
188 log.E.Ln("fatal error generating npub: %s", e)
189 break out
190 }
191 fmt.Printf("\rgenerating key: %s", r.npub)
192 switch where {
193 case PositionBeginning:
194 if bytes.HasPrefix(r.npub, append(prefix, []byte(str)...)) {
195 found = true
196 // Signal quit by sending result
197 select {
198 case resC <- r:
199 default:
200 }
201 return
202 }
203 case PositionEnding:
204 if bytes.HasSuffix(r.npub, []byte(str)) {
205 found = true
206 select {
207 case resC <- r:
208 default:
209 }
210 return
211 }
212 case PositionContains:
213 if bytes.Contains(r.npub, []byte(str)) {
214 found = true
215 select {
216 case resC <- r:
217 default:
218 }
219 return
220 }
221 }
222 }
223 }
224
225 func Gen() (skb, pkb []byte, err error) {
226 sign, err := p8k.New()
227 if err != nil {
228 return nil, nil, err
229 }
230 if err = sign.Generate(); chk.E(err) {
231 return
232 }
233 skb, pkb = sign.Sec(), sign.Pub()
234 return
235 }
236