├── LICENSE ├── .gitignore ├── vanity_npub.py └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 kdmukai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | python-nostr/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | python-nostr/ 133 | -------------------------------------------------------------------------------- /vanity_npub.py: -------------------------------------------------------------------------------- 1 | """ 2 | Nostr vanity pubkey generator 3 | 4 | https://github.com/kdmukai/nostr_vanity_npub 5 | """ 6 | import datetime 7 | from threading import Event, Lock, Thread 8 | from typing import List 9 | from nostr.key import PrivateKey 10 | 11 | 12 | 13 | class ThreadsafeCounter: 14 | """ 15 | Copied from SeedSigner 16 | """ 17 | def __init__(self, initial_value: int = 0): 18 | self.count = initial_value 19 | self._lock = Lock() 20 | 21 | @property 22 | def cur_count(self): 23 | # Reads don't require the lock 24 | return self.count 25 | 26 | def increment(self, step: int = 1): 27 | # Updates must be locked 28 | with self._lock: 29 | self.count += step 30 | 31 | def set_value(self, value: int): 32 | with self._lock: 33 | self.count = value 34 | 35 | 36 | 37 | class BruteForceThread(Thread): 38 | def __init__(self, targets: List[str], bonus_targets: List[str], threadsafe_counter: ThreadsafeCounter, event: Event, include_end: bool = False): 39 | super().__init__(daemon=True) 40 | self.targets = targets 41 | self.bonus_targets = bonus_targets 42 | self.threadsafe_counter = threadsafe_counter 43 | self.event = event 44 | self.include_end = include_end 45 | 46 | 47 | def run(self): 48 | i = 0 49 | while not self.event.is_set(): 50 | i += 1 51 | pk = PrivateKey() 52 | npub = pk.public_key.bech32()[5:] # Trim the "npub1" prefix 53 | 54 | # First check the bonus targets 55 | for target in self.bonus_targets: 56 | if npub[:len(target)] == target or (self.include_end and npub[-1*len(target):] == target): 57 | # Found one of our bonus targets! 58 | print(f"BONUS TARGET: {target}:\n\t{pk.public_key.bech32()}\n\t{pk.bech32()}", flush=True) 59 | 60 | # Now check our main targets 61 | for target in self.targets: 62 | if npub[:len(target)] == target or (self.include_end and npub[-1*len(target):] == target): 63 | # Found our match! 64 | print(f"\n{int(self.threadsafe_counter.cur_count):,} | {(time.time() - start):0.1f}s | {target} | npub1{npub}") 65 | print(f"""\n\t{"*"*76}\n\tPrivate key: {pk.bech32()}\n\t{"*"*76}\n""", flush=True) 66 | 67 | # Set the shared Event to signal to the other threads to exit 68 | self.event.set() 69 | break 70 | 71 | # Nothing matched 72 | if i % 1e4 == 0: 73 | # Accumulate every 1e4... 74 | self.threadsafe_counter.increment(1e4) 75 | if self.threadsafe_counter.cur_count % 1e6 == 0: 76 | # ...but update to stdout every 1e6 77 | print(f"{str(datetime.datetime.now())}: Tried {int(self.threadsafe_counter.cur_count):,} npubs so far", flush=True) 78 | continue 79 | 80 | 81 | 82 | 83 | if __name__ == "__main__": 84 | import argparse 85 | import time 86 | from threading import Event 87 | from nostr import bech32 88 | 89 | 90 | parser = argparse.ArgumentParser( 91 | description="********************** Nostr vanity pubkey generator **********************\n\n" + \ 92 | "Search for `target` in an npub such that:\n\n" + \ 93 | "\tnpub1[target]acd023...", 94 | formatter_class=argparse.RawTextHelpFormatter, 95 | ) 96 | 97 | # Required positional arguments 98 | parser.add_argument('targets', type=str, 99 | help="The string(s) you're looking for (comma-separated, no spaces)") 100 | 101 | # Optional arguments 102 | parser.add_argument('-b', '--bonus_targets', 103 | default="", 104 | dest="bonus_targets", 105 | help="Additional targets to search for, but does not end execution when found (comma-separated, no spaces)") 106 | 107 | parser.add_argument('-e', '--include-end', 108 | action="store_true", 109 | default=False, 110 | dest="include_end", 111 | help="Also search the end of the npub") 112 | 113 | parser.add_argument('-j', '--jobs', 114 | default=2, 115 | type=int, 116 | dest="num_jobs", 117 | help="Number of threads (default: 2)") 118 | 119 | args = parser.parse_args() 120 | targets = args.targets.lower().split(",") 121 | bonus_targets = args.bonus_targets.lower().split(",") if args.bonus_targets else [] 122 | include_end = args.include_end 123 | num_jobs = args.num_jobs 124 | 125 | print(targets) 126 | print(bonus_targets) 127 | 128 | for target in targets + bonus_targets: 129 | for i in range(0, len(target)): 130 | if target[i] not in bech32.CHARSET: 131 | print(f"""ERROR: "{target[i]}" is not a valid character (not in the bech32 charset)""") 132 | print(f"""\tbech32 chars: {"".join(sorted(bech32.CHARSET))}""") 133 | exit() 134 | 135 | if max([len(t) for t in targets]) >= 6: 136 | print( 137 | "This will probably take a LONG time!\n\n" + \ 138 | "\tTip: CTRL-C to abort.\n\n" 139 | ) 140 | 141 | start = time.time() 142 | threadsafe_counter = ThreadsafeCounter() 143 | event = Event() 144 | 145 | threads = [] 146 | for i in range(0, num_jobs): 147 | brute_force_thread = BruteForceThread(targets, bonus_targets, threadsafe_counter=threadsafe_counter, event=event, include_end=include_end) 148 | brute_force_thread.start() 149 | threads.append(brute_force_thread) 150 | 151 | print(f"Initialized {num_jobs} threads") 152 | 153 | print(f"{str(datetime.datetime.now())}: Starting") 154 | 155 | # Block until the first thread exits; after one thread finds a match, it will set the 156 | # Event and all threads will exit. 157 | threads[0].join() 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nostr Vanity `npub` Generator 2 | 3 | Python-based brute-force search for a given vanity `npub` target. 4 | 5 | 6 | ## Example 1 7 | ``` 8 | python3 vanity_npub.py h0dl 9 | ``` 10 | 11 | Searches for an `npub` that starts with: 12 | 13 | npub1 h0dl 023acd... 14 | 15 | On success, it will print: 16 | ``` 17 | 371,748 | 37.2s | npub1h0dl0dkjp884wz3wd30ucvfvdfvfdzp626hlvp68v0lfngnscdjsuy8yqm 18 | 19 | **************************************************************************** 20 | Private key: nsec1uu85rpk7sw2u2vrr6s9vlyvt4u58tcnluelm48a3fuytytvt4lnsa7pe0e 21 | **************************************************************************** 22 | ``` 23 | 24 | This reports the number of `npubs` it brute-force generated in order to find one with a matching prefix, the elapsed time in seconds, the successful `npub`, and its associated private key/`nsec`. 25 | 26 | 27 | ## Example 2 28 | Search for "h0dl" at the beginning or the end of the `npub`: 29 | ``` 30 | python3 vanity_npub.py -e h0dl 31 | > 1,026,042 | 103.0s | npub1y3ukxwznzysahdpnhrzgntal8kvmmd7uhx3k6klzzvaemm6nmwdse9h0dl 32 | ``` 33 | 34 | ## Example 3 35 | Search for "h0dl" or "sat0shi": 36 | ``` 37 | # Exits on whichever one it finds first 38 | python3 vanity_npub.py h0dl,sat0shi 39 | ``` 40 | 41 | ## Example 4 42 | Spend a lot of time searching for "nakam0t0", but along the way note any "h0dler" or "h0rnet" bonus matches: 43 | ``` 44 | python3 vanity_npub.py nakam0t0 -b h0dler,h0rnet 45 | ``` 46 | 47 | ## Usage 48 | ``` 49 | usage: vanity_npub.py [-h] [-b BONUS_TARGETS] [-e] [-j NUM_JOBS] targets 50 | 51 | ********************** Nostr vanity pubkey generator ********************** 52 | 53 | Search for `target` in an npub such that: 54 | 55 | npub1[target]acd023... 56 | 57 | positional arguments: 58 | targets The string(s) you're looking for (comma-separated, no spaces) 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | -b BONUS_TARGETS, --bonus_targets BONUS_TARGETS 63 | Additional targets to search for, but does not end execution when found (comma-separated, no spaces) 64 | -e, --include-end Also search the end of the npub 65 | -j NUM_JOBS, --jobs NUM_JOBS 66 | Number of threads (default: 2) 67 | ``` 68 | 69 | ## Limitations 70 | `npub`s can only include characters from the bech32 list: 71 | ``` 72 | 023456789acdefghjklmnpqrstuvwxyz 73 | ``` 74 | 75 | You'll get an error if you enter a vanity target that includes an unsupported character. 76 | ``` 77 | python vanity_npub.py bitcoin 78 | > ERROR: "b" is not a valid character (not in the bech32 charset) 79 | > bech32 chars: 023456789acdefghjklmnpqrstuvwxyz 80 | ``` 81 | 82 | --- 83 | 84 | 85 | ## Search is exponential! 86 | I can't do the probability math, but searching for a target string that's 6-chars long is exponentially harder than finding one that's 5-chars long. And then a target string that's 7-chars long is exponentially harder than 6. And so on. 87 | 88 | At 8 or 9+ characters you're going to need a *considerable* amount of work to yield a match. Days? Weeks? 89 | 90 | 91 | ## Search is random! 92 | You may have found a 5-character `npub` in 200s but the exact same 5-character target could take you 10x longer on the next run. It's just like bitcoin mining; the probabilities are one thing but there are no guarantees about your luck in any given run. 93 | 94 | Also note that you can stop a vanity `npub` search and restart it later. You're not wasting any work; it'll just keep searching new random `npub`s when you resume. 95 | 96 | 97 | ## Performance 98 | A 5-char vanity target could easily take tens of millions of tries. 6-char targets are in the hundreds of millions of tries. The script outputs an update at each million `npub`s tried so you can get a sense of your machine's guessing rate. 99 | 100 | It's also to your advantage to add additional `targets` or `bonus_targets`. You're already doing the work to generate a random `npub` so you may as well check it for more than one possible match. Each extra term only adds minimal additional effort. 101 | 102 | The biggest speed gain is to just open multiple terminal sessions and run an instance of `vanity_npub.py` in each one. Each instance will add more burden on the CPU but they don't seem to impact each others' performance much, as long as your CPU isn't completely slammed. 103 | 104 | The built-in attempt to leverage multithreading using the `-j` command line option yielded only modest gains. Perhaps future improvements could make this more effective. 105 | 106 | M1 Macbook Pro showed no benefit from multithreading (j=1 or j=2) at around 104s/mil tries. Slowed down beyond j=2. 107 | ``` 108 | # -j 1 or -j 2 109 | 1876.9s: Tried 18,000,000 npubs so far 110 | 1981.2s: Tried 19,000,000 npubs so far (105s / mil) 111 | 2085.5s: Tried 20,000,000 npubs so far (104s / mil) 112 | 2189.9s: Tried 21,000,000 npubs so far (104s / mil) 113 | ``` 114 | 115 | An older 8-core Ryzen 7 ran slightly faster at j=2 at around 160s/mil. Slowed down beyond j=2. 116 | 117 | 118 | 119 | ## Is this secure? 120 | Quick version: No, you shouldn't blindly trust any private key generator you found online. 121 | 122 | Long version: Assuming the pk calcs are trustworthy, it won't matter if two people search for the same vanity prefix. For example: 123 | 124 | ``` 125 | python3 vanity_npub.py hfsp 126 | > 365,386 | 36.3s | npub1hfspw0hcddc9k058ap4frvsexlykqzw3k2cun9430hxdvg8z9evqp5wfc8 127 | > 128 | > **************************************************************************** 129 | > Private key: nsec1d7smkrh9z8pn28lv6vm5ad736ms44wx84z405hsph6q7v3vqju0qqum9ak 130 | > **************************************************************************** 131 | 132 | python3 vanity_npub.py hfsp 133 | > 792,574 | 80.0s | npub1hfsp4ue6z0ykvjg99df6s2nvw7xzw9677wx70ydhdqegatf0qkcs4yqht7 134 | > 135 | > **************************************************************************** 136 | > Private key: nsec1949kx7knea9tcug9906u0l787vkxuha0yymh0vuu9x8xqmqvalaqk6lutn 137 | > **************************************************************************** 138 | ``` 139 | 140 | The `npub` is 58-characters long (not including the "npub1" prefix). That is more than enough randomness to make it effectively impossible for any two people to yield the exact same complete `npub` when searching for the same vanity prefix. 141 | 142 | --- 143 | 144 | ## Installation 145 | Requires: 146 | * python3.6+ 147 | * git 148 | 149 | Clone this repo: 150 | ``` 151 | git clone https://github.com/kdmukai/nostr_vanity_npub.git 152 | cd nostr_vanity_npub 153 | ``` 154 | 155 | Clone the [python-nostr](https://github.com/jeffthibault/python-nostr) dependency: 156 | ``` 157 | git clone https://github.com/jeffthibault/python-nostr.git 158 | pip install -e python-nostr 159 | ``` 160 | --------------------------------------------------------------------------------