Skip to content

Instantly share code, notes, and snippets.

@0xpatrickdev
Created December 31, 2024 22:01
Show Gist options
  • Save 0xpatrickdev/282e02641423b69d3c95d21e5a037c76 to your computer and use it in GitHub Desktop.
Save 0xpatrickdev/282e02641423b69d3c95d21e5a037c76 to your computer and use it in GitHub Desktop.
bech32 vanity addresses
#!/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);
});
}
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 });
}
});
@0xpatrickdev
Copy link
Author

0xpatrickdev commented Jan 10, 2025

./scripts/make-vanity-addr.js -p agoric -s 0rch -b 32 -w 2
Target suffix '0rch' (4 chars):
Expected attempts: 1,048,576
Estimated time at 100k/sec: 11 seconds
Starting search with 2 workers...
Looking for: agoric10rch...
Tried 386000 addresses (385614/sec)...
Tried 799000 addresses (399300/sec)...
Tried 1206000 addresses (401732/sec)...
Tried 1618000 addresses (404196/sec)...

Found match!
Address: agoric10rchcts29t5zsxmalc3dezc2zy0r2xsj5l0vwy6f6f3qxwgyxvwq9wpwr7
Public Key: dda9edd8c6675300ae32cc5b77d54b681b8a5b8ca704015a4604fb999b6e354f

./scripts/make-vanity-addr.js -p agoric -s setl -b 32 -w 2
Target suffix 'setl' (4 chars):
Expected attempts: 1,048,576
Estimated time at 100k/sec: 11 seconds
Starting search with 2 workers...
Looking for: agoric1setl...

Found match!
Address: agoric1setl2y53ru4tfjac7u3znfwvaqp3wldruj9kyzze8lk8z8q0h4jqwkahka
Public Key: 18ff1ca4cce6e1244a734e18c7839cef3acd7e0dad44d631f8759800780212f9

./scripts/make-vanity-addr.js -p agoric -s p00l -b 32 -w 2
Target suffix 'p00l' (4 chars):
Expected attempts: 1,048,576
Estimated time at 100k/sec: 11 seconds
Starting search with 2 workers...
Looking for: agoric1p00l...
Tried 402000 addresses (401598/sec)...

Found match!
Address: agoric1p00l7ywp8zev3f9txlcphrfx4nff0gkm0tkvyunfes7d8f5p670q76wweg
Public Key: 4373bb0123a3fdfb34c3a28bb8cbeaf0518eb32bf70e1e6ec416bcd8cdb6eeff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment