Not gonna lie, I put off on learning Rust for over 6 years, not because I didn't like it - I love it - like more than Go. 🦀
I Put Rust 🦀 To A Test 🐿️
The story begins by me wanting a vanity address for my XRP wallet. I looked at what the ridiculous service provider, Xaman, provides for Vanity Addresses, and they are almost $50 each, and Xaman knows your private key. Big no no for me regardless of the trust that Xaman may have earned. Buying a wallet address sounded ridiculous when I knew how to do the code myself.
In order to demonstrate this capability, Xaman decided to release a NodeJS version of the vanity address finder that had limited capabilities to it. This script was single-threaded only and would calculate roughly 60K seeds per second while it was looking for a vanity address. This was a solid starting point, and it really only required me to find a compatible cryptography library between NodeJS and Go in order for me to take this task and speed it up using the power of Go.
I pulled open the code editor and began writing the Go program that would later come to accomplish 400K seeds per second, a 6.66x faster seed search time. However, my curiosity didn't stop there. Some vanity addresses are really hard to find, to the point that I have run multiple iterations looking for a specific vanity address, and I've literally scanned over 3,000,000,000,000 (three trillion) seeds to find a match - and still no match yet, so I was kind of like "400K/sec isn't fast enough..."
When Rust Wins
Go wins over NodeJS hands down every day regardless of the use case. If it's front-end development, don't use Javascript or Typescript if you like your life. That's what I've learned. Rather, compiled and predictable GUI interfaces are stable for long term use far longer than any JS related technology could ever dream of. Go wins because its faster, but I couldn't figure out how to speed it up past 400K/s. This is where rust enters.
After six (6) years of learning Go and writing programs and packages in Go, I have decided that the first project that I would port over to Rust would be one that could only win by the performance benefits of Rust to prove what Rust can accomplish. The parody between the Go code and the Rust code was shockingly well transitioned and the core concepts behind Go and Rust actually worked out well together.
f, b, e := len(*config.String(cKeyFind)), len(*config.String(cKeyBegins)), len(*config.String(cKeyEnds))
if f > 0 && b == 0 && e == 0 { // only -find
log.Printf("Searching for r...%s... with %d/%d processors.", *config.String(cKeyFind), cores, runtime.NumCPU())
} else if f == 0 && b > 0 && e == 0 { // only -begins
log.Printf("Searching for r%s... with %d/%d processors.", *config.String(cKeyBegins), cores, runtime.NumCPU())
} else if f == 0 && b == 0 && e > 0 { // only -ends
log.Printf("Searching for r...%s with %d/%d processors.", *config.String(cKeyEnds), cores, runtime.NumCPU())
} else if f > 0 && b > 0 && e == 0 { // combined -find -begins
log.Printf("Searching for r%s...%s... with %d/%d processors.", *config.String(cKeyBegins), *config.String(cKeyFind), cores, runtime.NumCPU())
} else if f > 0 && b == 0 && e > 0 { // combined -find -ends
log.Printf("Searching for r...%s...%s with %d/%d processors.", *config.String(cKeyFind), *config.String(cKeyEnds), cores, runtime.NumCPU())
} else if f == 0 && b > 0 && e > 0 { // combined -begins -ends
log.Printf("Searching for r%s...%s with %d/%d processors.", *config.String(cKeyBegins), *config.String(cKeyEnds), cores, runtime.NumCPU())
} else if f > 0 && b > 0 && e > 0 {
log.Printf("Searching for r%s...%s...%s with %d/%d processors.", *config.String(cKeyBegins), *config.String(cKeyFind), *config.String(cKeyEnds), cores, runtime.NumCPU())
} else {
log.Fatal("Unsupported combination of -find -begins -ends provided.")
}
c := make(chan Trial, cores)
for i := 0; i < cores; i++ {
go search(c, matcher)
}
And for the Rust code:
if find_len > 0 && begins.is_empty() && ends.is_empty() {
println!(
"Searching for r...{}... with {}/{} processors.",
find, cores, cores_available
);
} else if find_len == 0 && !begins.is_empty() && ends.is_empty() {
println!(
"Searching for r{}... with {}/{} processors.",
begins, cores, cores_available
);
} else if find_len == 0 && begins.is_empty() && !ends.is_empty() {
println!(
"Searching for r...{} with {}/{} processors.",
ends, cores, cores_available
);
} else if find_len > 0 && !begins.is_empty() && ends.is_empty() {
println!(
"Searching for r{}...{}... with {}/{} processors.",
begins, find, cores, cores_available
);
} else if find_len > 0 && begins.is_empty() && !ends.is_empty() {
println!(
"Searching for r...{}...{} with {}/{} processors.",
find, ends, cores, cores_available
);
} else if find_len == 0 && !begins.is_empty() && !ends.is_empty() {
println!(
"Searching for r{}...{} with {}/{} processors.",
begins, ends, cores, cores_available
);
} else if find_len > 0 && !begins.is_empty() && !ends.is_empty() {
println!(
"Searching for r{}...{}...{} with {}/{} processors.",
begins, find, ends, cores, cores_available
);
} else {
eprintln!("Unsupported combination of parameters.");
std::process::exit(1);
}
// ...
let app_config = Arc::new(AppConfig {
algorithm,
begins,
ends,
find_len,
onep: use_onep,
vault: cli.vault.clone(),
found: found_set,
});
// Channel for found trials
let (tx, rx) = mpsc::channel::<Trial>();
let count = Arc::new(AtomicU64::new(0));
// Spawn worker threads
for _ in 0..cores {
let tx = tx.clone();
let matcher = Arc::new(matcher.clone());
let config = Arc::clone(&app_config);
let count = Arc::clone(&count);
thread::spawn(move || {
search(tx, matcher, config, count);
});
}
// Signal handler
ctrlc::set_handler(|| {
RUNNING.store(false, Ordering::SeqCst);
})
.expect("Error setting Ctrl-C handler");
let start = Arc::new(Instant::now());
let count_for_status = Arc::clone(&count);
let start_for_status = Arc::clone(&start);
// Status printing thread
thread::spawn(move || {
loop {
if !RUNNING.load(Ordering::Relaxed) {
break;
}
thread::sleep(Duration::from_secs(1));
let num = count_for_status.load(Ordering::Relaxed);
let elapsed = start_for_status.elapsed().as_secs_f64();
if elapsed > 0.0 {
let rate = (num as f64 / elapsed).floor() as u64;
let num_str = format_with_commas(num);
let rate_str = format_with_commas(rate);
let prefix = format!("Tested: {} seeds at {}/sec", num_str, rate_str);
let width = get_terminal_width();
let spaces_len = if width > prefix.len() + 2 {
width - prefix.len() - 2
} else {
0
};
let spaces = " ".repeat(spaces_len);
print!("\r{}{}", prefix, spaces);
let _ = io::stdout().flush();
}
}
});
As you can see, there is a significant difference in how the go search() is replaced in rust, but regardless, its still pretty straight forward to me and acceptable complexity given what it accomplishes.
Just exactly does this difference mean? Well, it comes out to about 15% actually. The Rust application can scan over 500K seeds per second whereas the Go application could only scan 400K seeds per second and the NodeJS / JavaScript application could only scan 60K seeds per second.
By going from NodeJS to Go, we increased the speed by over 6666%, by going from NodeJS to Rust, we increased the speed by over 8416%. What does this mean? I was able to gather over 30+ vanity addresses within seconds and didn't have to include Xaman in the process at all. Mission Accomplished.
Why I Built This
Xaman rugged their PRO subscriptions and begun charging me over 70,000% fees over the base XRPL transaction fee in order to subsidize their development costs. What this translated to was me being displaced from the XRP ledger because I didn't want to be sending Xaman 0.09 XRP per transaction that didn't move any value around, and then over 10 XRP on some larger transactions when they saw they could make a quick buck on me. I am mad at them for that. I feel back stabbed and betrayed by greed at the cost of innovation and correctness. Thus, I took from them their ability to charge their customers $50 per vanity address by releasing a tool that does it for free and keeps the greedy folks at Xaman away from your private keys at the same time.
andreimerlescu
/
rusty-xrp-addr
A rust utility for finding a vanity XRP wallet address that works with all XRP ledger wallets.
find-xrp-addr
High-performance XRP Ledger vanity address finder written in Rust.
Searches for custom r... addresses matching a substring (--find), prefix (--begins), suffix (--ends), or any combination, using all available CPU cores.
Features
- Multi-threaded (configurable cores)
- ED25519 (default, recommended) and secp256k1 support
- Case-insensitive or sensitive matching
- Optional saving to 1Password via op CLI (--1p)
- Live progress (seeds tested + rate/sec)
- Graceful shutdown on Ctrl-C
- Validates against XRPL alphabet and prevents confusing characters (I, O, l, 0)
Getting Started / Compilation
Prerequisites
- Rust toolchain (stable) - install from https://rustup.rs
- For --1p: 1Password CLI (op) installed and authenticated
Build
make build
Or manually:
cargo build --release
The release binary will be at bin/find-xrp-addr (after make build).
Run
make run ARGS="--find test --cores 8"
Or directly:
./bin/find-xrp-addr --find "abc" --insensitive
Common examples:
- Search containing "test": ./bin/find-xrp-addr --find test
- Begins with "abc" after r: ./bin/find-xrp-addr --begins abc
- Ends with "xyz": ./bin/find-xrp-addr --ends xyz
- Combined…
Top comments (0)