Skip to content

Instantly share code, notes, and snippets.

@kdmukai
Last active February 12, 2024 00:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kdmukai/2b78776814786c9dc36094e3967bc2c2 to your computer and use it in GitHub Desktop.
Save kdmukai/2b78776814786c9dc36094e3967bc2c2 to your computer and use it in GitHub Desktop.
How to generate vanity seed fingerprints, bitcoin addresses, and txids

How to generate vanity seed fingerprints, bitcoin addresses, and txids

Note: This is all just for educational / fun purposes. Do not use this code to generate a real seed that you intend to store real value on!!

Setup

Create a python3 virtualenv and install the one dependency:

pip install embit

Grind for vanity fingerprint

Grid for a target seed fingerprint, with option to supply the first n words of the mnemonic.

Note: Again, DO NOT USE THESE SEEDS TO SECURE REAL VALUE!!!

from grind import grind_mnemonic
grind_mnemonic(target="decaf", prepend_mnemonic="coffee power remove".split())

It'll keep returning candidate matches until you CTRL-C.

So you can follow along, my mnemonic was:

coffee power remove satoshi tunnel ice either nose action fancy truck exist

fingerprint: decaf1ab

When you find a vanity fingerprint you like, move on to:

Grind for vanity address

This is the most resource-intensive step!

from grind import grind_address
grind_address(mnemonic, target="decaf", start=0, account=0)

This is a linear search that increments the address derivation path:

m/49'/0'/{account}'/0/{start + i}

And prints out each case-insensitive matching address that is found.

Because this is a linear search, if you interrupt the process, use the start param to pick up where you left off.

Run this in multiple processes over different account values in parallel to speed up the odds of landing a match.

Here's my result:

m/49'/0'/4'/0/53499628 3DecafHwmLTN7xShDtjcTZj8MfMnpbVQLS

Grind for vanity txid

With your mnemonic and vanity address in hand, fund the address with enough sats to cover the OP_RETURN tx we'll be creating next.

The OP_RETURN data will be appended with a random hex string which we keep changing so that each iteration hashes to a different txid until the vanity target is found.

Set up your params and start grinding. This step will be a bit quicker as we're back in the hexadecimal domain, though each iteration does require some heavier calculations since we have to construct and sign a proposed tx in order to get to the final txid that would appear onchain.

from grind_txid import grind_txid
kwargs = dict(
    mnemonic = "<your mnemonic here>",
    input_txid = "<your txid that funded your vanity addr>",
    input_vout = 0,  # the output number from the above tx that went to your vanity addr
    input_amount = 7639,  # total sats sent to your vanity addr via that funding tx
    cur_block_height = 830_009,  # update to be at least equal to the block that contains the funding tx
    vanity_target = "decaf",  # The hexadecimal string you'd like your OP_RETURN tx to start with 
    op_return_msg = "gist.github.com/kdmukai/2b78776814786c9dc36094e3967bc2c2",
    derivation_account = 4,       # From the results of the vanity addr derivation path
    derivation_index = 53499628,  # From the results of the vanity addr derivation path
)
grind_txid(**kwargs)

The resulting transaction can be directly pasted into Sparrow (or any other software coordinator) and immediately broadcast since it will already be signed.

My run yielded:

decaf5911733de701cbf1ab76050d5531ffa15e4feaed18dc62a7aef8a766bd5

0200000000010134e675fb1623fb78e82fe67471729f5428d815e72e9a788527667327bd3de2670000000017160014009deaf737d9bc1c058bdfaaba8248d608451d16ffffffff010000000000000000466a4c43676973742e6769746875622e636f6d2f6b646d756b61692f3262373837373638313437383663396463333630393465333936376263326332207c20613061336163373702473044022057d1d1561396c65b330da1cfbe65b7814d635c329957451fd334ce019257c5ff02206964aaccde963187d728efa4166955df036bddb62057f0ba96532d846c9feb670121026e539670b341de8c263dce3627f339a5fee799dfaf81305643aee126090746013baa0c00

And here it is onchain: https://mempool.space/tx/decaf5911733de701cbf1ab76050d5531ffa15e4feaed18dc62a7aef8a766bd5

import random
from binascii import hexlify
from embit import bip39, bip32, script
def grind_mnemonic(target="decaf", prepend_mnemonic=[]):
"""
Generate a BIP-39 mnemonic with a vanity fingerprint using an optional list of words
to prepend.
Prints any mnemonic where the target is found in the fingerprint.
WARNING: weak entropy, NOT meant to secure real value!!
WARNING: even weaker if you provide a prepend_mnemonic!!
e.g.:
grind_mnemonic("decaf", ["coffee", "power", "remove"])
Finds:
"08decaf0" / coffee power remove solution vacuum poverty stumble matrix left sauce paddle soup
"1badecaf" / coffee power remove aerobic fragile dial mushroom insect install rough purity leg
"decaf1ab" / coffee power remove satoshi tunnel ice either nose action fancy truck exist
"""
for c in target:
if c not in "0123456789abcdef":
raise ValueError("Target must be hex string")
for word in prepend_mnemonic:
if word not in bip39.WORDLIST:
raise ValueError(f"prepend_mnemonic word '{word}' not in BIP39 wordlist")
print(f"\n\nFingerprint starting with \"{target}\" is 1/{16**len(target):,} odds.")
print(f"Fingerprint that includes \"{target}\" is 1/{int(16**len(target) / (8 - len(target))):,}(?) odds.\n")
# Convert optional prepended words' indices to 11-bit binary strings
prepend_bits = "".join([("0" * 11 + bin(bip39.WORDLIST.index(w))[2:])[-11:] for w in prepend_mnemonic])
# Ensure random.seed is reset to a new value to avoid re-grinding the same "random"
# bytes in subsequent runs.
random.seed()
iterations = 0
while True:
iterations += 1
if iterations % 10000 == 0:
print(iterations)
# Generate new, random entropy but prepend with (optional) provided words
rand_bits = prepend_bits + ((("0" * 128) + bin(random.getrandbits(128))[2:])[-128:])[:128 - len(prepend_bits)]
rand_bytes = int(rand_bits, 2).to_bytes(len(rand_bits) // 8, 'big')
mnemonic = bip39.mnemonic_from_bytes(rand_bytes)
root = bip32.HDKey.from_seed(bip39.mnemonic_to_seed(mnemonic))
fingerprint = hexlify(root.child(0).fingerprint).decode()
if target in fingerprint:
print(iterations, fingerprint, mnemonic)
def grind_address(mnemonic: str, target: str, start: int = 0, account: int = 0):
"""
Grind for a target nested segwit vanity address for the specified derivation path
account. The leading "3" in the address is ignored. Vanity search is case-insensitive.
Resume the brute-force search for the given account via the `start` index.
Derivation path is: m/49'/0'/{account}'/0/{start + i}
e.g.:
grind_address(mnemonic, target="decaf", start=0, account=4)
Finds:
("m/49'/0'/4'/0/4459869", '3DEcaFeP3sMoJp7gr5JssGxWKQHZyFcwzH')
("m/49'/0'/4'/0/13141195", '3DECaFGTVPfgLVxmcueNV9zax1EPsFmpAz')
("m/49'/0'/4'/0/36737304", '3DECafepKqncE42KmknLqQEdFxS6YCMcZW')
("m/49'/0'/4'/0/40071612", '3DecaFwdh6n8sLsVR3h7sWr9aijHXgX9r6')
("m/49'/0'/4'/0/45526285", '3DECAfpxHASaNqWj9eWemtWhaD7E2wpQp7')
("m/49'/0'/4'/0/52847357", '3DEcaFh5gGXH25f1GVjR1EUoYU4MjBFwBS')
("m/49'/0'/4'/0/53499628", '3DecafHwmLTN7xShDtjcTZj8MfMnpbVQLS')
"""
for c in target:
# must be a valid base58 character: 1-9, A-Z, a-z, excluding 0, O, I, l
if c not in "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz":
raise ValueError(f"Target must be a valid base58 string; '{c}' is not allowed.")
root = bip32.HDKey.from_seed(bip39.mnemonic_to_seed(mnemonic))
derivation_path = f"m/49'/0'/{account}'"
xprv = root.derive(path=derivation_path)
xpub = xprv.to_public()
target = f"3{target.lower()}"
max_index = 2**31 - 1
matches = []
try:
for i in range(start, max_index):
if i % 100000 == 0:
print(f"{i:,}; found {len(matches)} in account {account}")
pubkey = xpub.derive([0,i]).key
addr = script.p2sh(script.p2wpkh(pubkey)).address()
if addr.lower().startswith(target):
full_path = f"{derivation_path}/0/{i}"
print(full_path, addr)
matches.append((full_path, addr))
except KeyboardInterrupt:
pass
finally:
print(f"\nEXITING: Searched addresses {start} - {i}.")
for derivation_path, addr in matches:
print(derivation_path, addr)
if not matches:
print(f"Searched ALL addresses in {derivation_path}; no match found.")
from collections import OrderedDict
import random
from binascii import unhexlify
from embit import bip32, bip39, compact
from embit.finalizer import finalize_psbt
from embit.script import p2sh, p2wpkh, Script
from embit.networks import NETWORKS
from embit.psbt import PSBT, InputScope, OutputScope, DerivationPath
from embit.transaction import TransactionInput, TransactionOutput, SIGHASH
class OPCODES:
OP_RETURN = 106
OP_PUSHDATA1 = 76
def grind_txid(mnemonic, input_txid: str, input_vout: int, input_value: int, cur_block_height: int, vanity_target: str = "decaf", op_return_msg: str = "", derivation_account: int = 0, derivation_index: int = 0, network: str = "main"):
"""
Grind an OP_RETURN txid that starts with a specific vanity string.
kwargs = dict(
mnemonic = "<your mnemonic here>",
input_txid = "<your txid that funded your vanity addr>",
input_vout = 0, # the output number from the above tx that went to your vanity addr
input_amount = 7639, # total sats sent to your vanity addr via that funding tx
cur_block_height = 830_009, # update to be at least equal to the block that contains the funding tx
vanity_target = "decaf", # The hexadecimal string you'd like your OP_RETURN tx to start with
op_return_msg = "gist.github.com/kdmukai/2b78776814786c9dc36094e3967bc2c2",
derivation_account = 4, # From the results of the vanity addr derivation path
derivation_index = 53499628, # From the results of the vanity addr derivation path
)
grind_txid(**kwargs)
"""
for c in vanity_target:
if c not in "0123456789abcdef":
raise ValueError("Invalid character in vanity_target")
root = bip32.HDKey.from_seed(bip39.mnemonic_to_seed(mnemonic))
fingerprint = root.child(0).fingerprint
print(f"Root fingerprint: {fingerprint.hex()}")
xprv = root.derive(path=f"""m/49'/{"0" if network == "main" else "1"}'/{derivation_account}'""")
xpub = xprv.to_public()
pubkey = xpub.derive([0, derivation_index]).key
script_pubkey = p2sh(p2wpkh(pubkey))
print(f"Funding address: {script_pubkey.address(NETWORKS[network])}")
# Create the PSBT from scratch
txinput = TransactionInput(txid=unhexlify(input_txid), vout=input_vout)
txoutput = TransactionOutput(value=input_value, script_pubkey=script_pubkey)
inp = InputScope(unknown={}, vin=txinput)
inp.witness_utxo = txoutput
inp.sighash_type = SIGHASH.ALL
inp.redeem_script = p2wpkh(pubkey)
HARDENED = 2147483648
bip32_derivations = OrderedDict()
bip32_derivations[pubkey] = DerivationPath(fingerprint=fingerprint, derivation=[49 + HARDENED, (0 if network == "main" else 1) + HARDENED, derivation_account + HARDENED, 0, derivation_index])
inp.bip32_derivations = bip32_derivations
psbt = PSBT()
psbt.inputs.append(inp)
psbt.locktime = cur_block_height
# Ensure random.seed is reset to a new value to avoid re-grinding the same "random"
# bytes in subsequent runs.
random.seed()
i = 0
while True:
# Add random bytes to the end of the OP_RETURN payload to make the txid unique each time
raw_payload_data = f"{op_return_msg} | {random.randbytes(4).hex()}".encode()
data = (compact.to_bytes(OPCODES.OP_RETURN) +
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
compact.to_bytes(len(raw_payload_data)) +
raw_payload_data)
output = OutputScope()
output.script_pubkey = Script(data)
output.value = 0
psbt.outputs.append(output)
# Must sign and finalize the psbt to get the final txid
psbt.sign_with(root)
final = finalize_psbt(psbt)
if final.txid().hex().startswith(vanity_target):
print(raw_payload_data)
print(final.txid().hex())
print("")
print(final)
break
psbt.outputs.clear()
i += 1
if i % 10000 == 0:
print(f"{i:,}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment