Created
December 31, 2024 22:01
-
-
Save 0xpatrickdev/282e02641423b69d3c95d21e5a037c76 to your computer and use it in GitHub Desktop.
bech32 vanity addresses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/* eslint-disable no-underscore-dangle */ | |
/* global setInterval clearInterval */ | |
import { Command } from 'commander'; | |
import { Worker } from 'worker_threads'; | |
import { fileURLToPath } from 'url'; | |
import { dirname } from 'path'; | |
import process from 'process'; | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = dirname(__filename); | |
const BECH32_CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; | |
const validateBech32Suffix = suffix => { | |
const invalidChars = suffix | |
.split('') | |
.filter(char => !BECH32_CHARS.includes(char)); | |
if (invalidChars.length > 0) { | |
throw new Error( | |
`Invalid bech32 characters in suffix: ${invalidChars.join(', ')}. Valid characters are: ${BECH32_CHARS}`, | |
); | |
} | |
return true; | |
}; | |
const predictAttempts = (suffix, minePrivateKey) => { | |
// Each character has 32 possibilities | |
const expectedAttempts = 32 ** suffix.length; | |
// Format with commas | |
const formatted = expectedAttempts.toLocaleString(); | |
// Calculate some time estimates at different rates | |
const rate = minePrivateKey ? 20_000 : 100_000; | |
const timeAt100k = expectedAttempts / rate; // seconds at 20 or 100k/sec (M1 Pro) | |
let timeFormatted = ''; | |
if (timeAt100k < 60) { | |
timeFormatted = `${Math.ceil(timeAt100k)} seconds`; | |
} else if (timeAt100k < 3600) { | |
timeFormatted = `${Math.ceil(timeAt100k / 60)} minutes`; | |
} else if (timeAt100k < 86400) { | |
timeFormatted = `${Math.ceil(timeAt100k / 3600)} hours`; | |
} else { | |
timeFormatted = `${Math.ceil(timeAt100k / 86400)} days`; | |
} | |
console.log(`Target suffix '${suffix}' (${suffix.length} chars):`); | |
console.log(`Expected attempts: ${formatted}`); | |
console.log(`Estimated time at ${rate / 1_000}k/sec: ${timeFormatted}`); | |
return expectedAttempts; | |
}; | |
const makeVanityAddress = async options => { | |
const { prefix, byteLength, suffix, workers, minePrivate } = options; | |
validateBech32Suffix(suffix); | |
predictAttempts(suffix, minePrivate); | |
const state = { | |
workers: [], | |
totalAttempts: 0, | |
startTime: Date.now(), | |
progressInterval: null, | |
BATCH_SIZE: 1000, | |
}; | |
const cleanup = () => { | |
if (state.progressInterval) { | |
clearInterval(state.progressInterval); | |
} | |
for (const worker of state.workers) { | |
worker.terminate(); | |
} | |
}; | |
const reportProgress = () => { | |
const elapsedSecs = (Date.now() - state.startTime) / 1000; | |
const rate = Math.floor(state.totalAttempts / elapsedSecs); | |
console.log(`Tried ${state.totalAttempts} addresses (${rate}/sec)...`); | |
}; | |
const handleWorkerMessage = (worker, message) => { | |
if (message.type === 'result') { | |
if (message.found) { | |
cleanup(); | |
console.log('\nFound match!'); | |
console.log(`Address: ${message.address}`); | |
console.log(`Public Key: ${message.pubKey}`); | |
message.privateKey && console.log(`Private Key: ${message.privateKey}`); | |
process.exit(0); | |
} | |
// Give worker new batch on completion | |
worker.postMessage({ batchSize: state.BATCH_SIZE }); | |
} else if (message.type === 'progress') { | |
state.totalAttempts += message.attempts; | |
} else if (message.type === 'error') { | |
console.error('Worker error:', message.error); | |
cleanup(); | |
process.exit(1); | |
} | |
}; | |
console.log(`Starting search with ${workers} workers...`); | |
console.log(`Looking for: ${prefix}1${suffix}...`); | |
// Start progress reporting | |
state.progressInterval = setInterval(reportProgress, 1000); | |
// Handle process termination | |
process.on('SIGINT', () => { | |
console.log('\nGracefully shutting down...'); | |
cleanup(); | |
process.exit(0); | |
}); | |
// Create and initialize workers | |
const workerScript = new URL('./vanity-addr-worker.js', import.meta.url); | |
state.workers = Array(workers) | |
.fill(null) | |
.map(() => { | |
const worker = new Worker(workerScript, { | |
workerData: { | |
prefix, | |
targetSuffix: suffix, | |
byteLength, | |
minePrivateKey: minePrivate, | |
}, | |
type: 'module', | |
}); | |
// Set up error handling for each worker | |
worker.on('error', error => { | |
console.error('Worker error:', error); | |
cleanup(); | |
process.exit(1); | |
}); | |
worker.on('exit', code => { | |
if (code !== 0) { | |
console.error(`Worker stopped with exit code ${code}`); | |
cleanup(); | |
process.exit(1); | |
} | |
}); | |
worker.on('message', message => handleWorkerMessage(worker, message)); | |
// Start initial work | |
worker.postMessage({ batchSize: state.BATCH_SIZE }); | |
return worker; | |
}); | |
}; | |
if (import.meta.url === `file://${__filename}`) { | |
const program = new Command(); | |
program | |
.name('vanity-address') | |
.description( | |
'Generate vanity cryptocurrency addresses with custom suffixes', | |
) | |
.option('-p, --prefix <string>', 'address prefix', 'agoric') | |
.option( | |
'-b, --byte-length <number>', | |
'byte length for key generation', | |
'32', | |
) | |
.option( | |
'-s, --suffix <string>', | |
'desired bech32 suffix to search for. valid characters are: qpzry9x8gf2tvdw0s3jn54khce6mua7l', | |
'0rch', | |
) | |
.option('-w, --workers <number>', 'number of worker threads to use', '1') | |
.option('--mine-private', 'mine private keys (slower)', false) | |
.addHelpText( | |
'after', | |
` | |
Examples: | |
# Generate address with default parameters | |
$ ${program.name()} | |
# Generate address with custom prefix and suffix | |
$ ${program.name()} --prefix cosmos --suffix moon --workers 2 | |
# Generate address with private key mining | |
$ ${program.name()} --prefix agoric --suffix star --workers 3 --mine-private | |
Notes: | |
- Valid bech32 characters for suffix: ${BECH32_CHARS} | |
- Higher number of workers may improve performance | |
- Private key mining is significantly slower than public key mining | |
`, | |
); | |
program.parse(); | |
const options = program.opts(); | |
// Convert string numbers to integers | |
options.byteLength = parseInt(options.byteLength, 10); | |
options.workers = parseInt(options.workers, 10); | |
makeVanityAddress(options).catch(e => { | |
console.error(e); | |
process.exit(1); | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { bech32 } from 'bech32'; | |
import { createHash, randomBytes } from 'crypto'; | |
import { parentPort, workerData } from 'worker_threads'; | |
import process from 'process'; | |
import { secp256k1 } from '@noble/curves/secp256k1'; | |
const createBech32Address = (prefix, payload) => { | |
const words = bech32.toWords(payload); | |
return bech32.encode(prefix, words); | |
}; | |
const generateAddress = (pubKey, prefix) => { | |
const hash = createHash('sha256').update(pubKey).digest(); | |
return createBech32Address(prefix, hash); | |
}; | |
const bufferToHex = buffer => { | |
return Array.from(new Uint8Array(buffer)) | |
.map(b => b.toString(16).padStart(2, '0')) | |
.join(''); | |
}; | |
const getKeyPair = byteLength => { | |
const privKey = randomBytes(byteLength); | |
const pubKey = secp256k1.getPublicKey(privKey); | |
return { privKey, pubKey }; | |
}; | |
const getPubKey = byteLength => randomBytes(byteLength); | |
function searchRange( | |
batchSize, | |
prefix, | |
targetPrefix, | |
byteLength, | |
minePrivateKey, | |
) { | |
for (let i = 0; i < batchSize; i += 1) { | |
let privKey; | |
let pubKey; | |
if (minePrivateKey) { | |
({ privKey, pubKey } = getKeyPair(byteLength)); | |
} else { | |
pubKey = getPubKey(byteLength); | |
} | |
const address = generateAddress(pubKey, prefix); | |
if (address.startsWith(targetPrefix)) { | |
return { | |
type: 'result', | |
found: true, | |
pubKey: bufferToHex(pubKey), | |
privateKey: privKey ? bufferToHex(privKey) : undefined, | |
address, | |
}; | |
} | |
} | |
return { type: 'result', found: false }; | |
} | |
const { prefix, targetSuffix, byteLength, minePrivateKey } = workerData; | |
const targetPrefix = `${prefix}1${targetSuffix}`; | |
parentPort.on('error', error => { | |
console.error('Worker error:', error); | |
process.exit(1); | |
}); | |
process.on('uncaughtException', error => { | |
console.error('Uncaught exception in worker:', error); | |
process.exit(1); | |
}); | |
parentPort.on('message', ({ batchSize }) => { | |
try { | |
const result = searchRange( | |
batchSize, | |
prefix, | |
targetPrefix, | |
byteLength, | |
minePrivateKey, | |
); | |
parentPort.postMessage({ type: 'progress', attempts: batchSize }); | |
parentPort.postMessage(result); | |
} catch (error) { | |
parentPort.postMessage({ type: 'error', error: error.message }); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.