Skip to content

bernardladenthin/BitcoinAddressFinder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,391 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Note

No IDE or local setup required. This repository is optimized for fully AI-assisted development using Claude Code. No local toolchain, no IDE, nothing to install — everything works completely through Claude.

AI:
Claude

Build:
Java 21+
JPMS
JUnit
JSpecify
NullAway
Checker Framework
Error Prone
Maven Enforcer
Lombok
jqwik
ArchUnit
SpotBugs
jcstress
Lincheck
vmlens
JMH
Publish
CodeQL

Coverage:
Coverage Status
codecov
JaCoCo
PIT Mutation

Quality:
Quality Gate
Code Smells
Security Rating

Security:

Known Vulnerabilities
FOSSA Status
Dependencies
OSV-Scanner

Package:
Maven Central
Snapshot
Release Date
Last Commit

License:
Apache 2.0 License

Community:
Contribute with Gitpod
OpenSSF Best Practices
OpenSSF Scorecard
Dependabot
Conventional Commits
Keep a Changelog
SemVer
REUSE
Maintained?
Issues
Pull Requests
GitHub Stars
Treeware
Stand With Ukraine

BitcoinAddressFinder

🚀 Fast address finder for Bitcoin and altcoins using OpenCL & Java – includes vanity address generation, balance checking, and offline support.


Table of Contents


About BitcoinAddressFinder

BitcoinAddressFinder is a free, high-performance tool for scanning random private keys across a wide range of cryptocurrencies — including Bitcoin, Bitcoin Cash, Bitcoin SV, Litecoin, Dogecoin, Dash, Zcash, and many more.

Its core purpose is to generate both compressed and uncompressed addresses with maximum efficiency, combining the portability of the Java Virtual Machine (JVM) with OpenCL-powered GPU acceleration.

Each generated address is checked against a high-speed LMDB database to detect whether it has ever been used — identifying possible balances, known keyspaces, and even RIPEMD160 hash collisions.

🔍 Whether you're generating vanity addresses, verifying address usage, or experimenting with cryptographic edge cases, BitcoinAddressFinder is built for speed, flexibility, and fully offline operation.

🔐 Runs air-gapped · ⚡ GPU-accelerated · 🧪 Unit-tested · 🛠️ Extensible

Made with ❤️ in Germany

Copyright (c) 2017-2025 Bernard Ladenthin

Requirements

  • Java 21 or newer is required to run BitcoinAddressFinder.
    Older versions such as Java 8, 11, or 17 are not supported.

  • 🚀 OpenCL (optional):
    You can run this software in either CPU-only mode or with GPU acceleration via OpenCL for significantly enhanced performance.

    When OpenCL is enabled:

    • Elliptic Curve Key Generation is offloaded to one or more OpenCL-capable devices (e.g., GPUs), dramatically boosting key scanning throughput.
    • SHA-256 and RIPEMD-160 hashing operations are also offloaded to the GPU, further reducing CPU load and increasing overall efficiency.
    • Multi-GPU setups are fully supported — each device can be configured individually, enabling efficient parallelization and scalability across multiple GPUs.

Quickstart

  1. Download Software
  1. Download and extract the light database from https://github.com/bernardladenthin/BitcoinAddressFinder#use-my-prepared-database
  2. Download a configuration set like:
  1. Put all in one directory like the following structure
  • Downloads
    • lmdb
      • data.mdb
      • lock.mdb
    • bitcoinaddressfinder-1.8.0-SNAPSHOT-jar-with-dependencies.jar
    • logbackConfiguration.xml
    • config_Find_1OpenCLDevice.js
    • run_Find_1OpenCLDevice.bat (Windows) / run_Find_1OpenCLDevice.sh (Linux/macOS)
  1. Run the launcher for your OS:
  • Windows: double-click / run run_Find_1OpenCLDevice.bat
  • Linux / macOS: chmod +x run_Find_1OpenCLDevice.sh && ./run_Find_1OpenCLDevice.sh

Every examples/run_*.bat has a matching examples/run_*.sh (same JVM flags); use the .sh on Linux/macOS and the .bat on Windows.

What a healthy run looks like

On startup the tool builds the GPU pre-filter from your database and then begins scanning. The log walks through four one-time build phases — reading → indexing → peeling → assigning — prints a ready line, and from then on emits a periodic Statistics line. That ticking Statistics line is how you know it is running correctly. Abridged real output (one line per phase):

… BinaryFuse8AddressPresence - Binary Fuse8 filter: reading 132288304 addresses from the source …
… BinaryFuse8AddressPresence - Binary Fuse8 filter: reading addresses: 99% (132288300/132288304), 15.4M/s, ETA 0s, elapsed 8s.
… BinaryFuse8AddressPresence - Binary Fuse8 filter: indexing:  99% (132207500/132207501), 22.2M/s, ETA 0s, elapsed 5s.
… BinaryFuse8AddressPresence - Binary Fuse8 filter: peeling:   99% (132207500/132207501), 12.6M/s, ETA 0s, elapsed 10s.
… BinaryFuse8AddressPresence - Binary Fuse8 filter: assigning: 99% (132207500/132207501), 21.1M/s, ETA 0s, elapsed 6s.
… BinaryFuse8AddressPresence - Binary Fuse8 filter: ready (132207501 addresses, 148766720 fingerprint slots).
… ConsumerJava - start consumeKeysRunner
… AbstractProducer - Init producer.
… ConsumerJava - Statistics: [uptime 0 min] [Generated 129 M/s (859 M total)] [-> LMDB 2 M/s (13 M lookups total), 99.22% pre-filtered] … [Hits: 0]

What to look for:

  • The four build phases run once, at startup (a few seconds each per 100 M addresses). This is the filter being built from your database — it is normal for the tool to sit here briefly before the first Statistics line, with no keys scanned yet.
  • ready (… addresses, … fingerprint slots) means the filter is built and scanning is about to start.
  • Statistics: … [Generated N/s …] [-> LMDB M/s …, NN.NN% pre-filtered] … [Hits: 0] is the running heartbeat. Generated is the raw key rate; -> LMDB is how many candidates survive the GPU pre-filter and reach the database; pre-filtered is the share the filter eliminated (so the CPU/LMDB never sees them). Hits: 0 is expected — a hit means a funded address was actually found.
  • The filter is named after your gpuFilterType: the excerpt above is a FUSE_8 run, whereas the recommended default FUSE_16 logs Binary Fuse16 filter: … and drives the pre-filtered share even higher (fewer survivors reach LMDB). See docs/filter-selection.md for the trade-off.

Tune the configuration for your own machine

Every grid and filter recommendation in this README and in docs/filter-selection.md was measured on one developer's hardware. The TuneConfiguration command measures yours:

# Use the launcher — it carries the --add-opens the JVM needs for LMDB access:
examples/run_TuneConfiguration.sh          # Linux/macOS
examples\run_TuneConfiguration.bat         # Windows

The bare java -jar … config_TuneConfiguration.json works only when no real database is configured. The moment lmdbConfigurationReadOnly.lmdbDirectory points at an actual LMDB, the lookup path reflects into java.nio/sun.nio.ch and the JVM throws InaccessibleObjectException without the --add-opens flags the launcher sets. Use the launcher, or copy its flag list, whenever you point the tuner at a database.

It sweeps batchSizeInBits × keysPerWorkItem, measures the net end-to-end throughput of each combination (candidate keys per second through the whole pipeline, not kernel time), and prints a table of every arm plus the winning configuration as a ready-to-paste JSON file. It also measures what one database lookup costs on your storage in its current state — the term that ranges from 4.1 µs warm to 292.7 µs cold, and the one the FUSE_8 / FUSE_16 choice hinges on. Every number in the report is labelled MEASURED (here, on this machine) or DOCUMENTED / ESTIMATED (a published constant), so it is always clear which is which.

Honest expectations:

  • It takes minutes. The run time is arms × (warmupSecondsPerArm + secondsPerArm); the defaults give 25 arms × 25 s ≈ 10 minutes, plus a one-time filter build (~44 s per 100 M entries).
  • It does not read or rebuild your database. The grid sweep builds its pre-filter from a deterministic PRNG source sized to targetDatabaseEntries — set that to the size of the database you intend to run against. Only the optional verification-cost stage touches LMDB, and only if lmdbConfigurationReadOnly.lmdbDirectory points at a real database. With no database configured the command still runs end to end and falls back to the documented lookup cost.
  • sweepFilterTypes is off by default, and should stay off unless you need it. Enabling it forces a second full filter build (~150 s at the 132 M Light DB tier, ~26 minutes at the 1.377 B Full DB tier) to measure the FUSE_8 / FUSE_16 choice instead of deriving it from total = probe + fpr × verification.

✨ Features

  • 📐 Supports blockchain addresses based on secp256k1
  • 🛡️ Unit-tested, trusted open source that can be compiled easily by yourself
  • 🎯 Vanity generation of Bitcoin addresses using regex patterns
  • 🔌 Runs entirely offline — no internet connection is required or used. Suitable for air-gapped systems and isolated environments — even in a bunker with a generator and zero connectivity.
  • 🤹 No synchronization required to run multiple instances. Random numbers are used, so no coordinated search strategy is needed — just run it on multiple machines
  • ⚡ Checks a high-performance database of known addresses to detect already used ones
  • 📦 Portable, platform-independent, runs on the JVM
  • 🔁 Generates both uncompressed and compressed keys simultaneously
  • 🧮 EC key generation via:
    • 🧵 Multiple CPU threads
    • 🖥️ Multiple OpenCL devices (optional)

⚡ ECC Scalar Multiplication

The OpenCL kernel generates keys with the same two techniques the fastest open-source secp256k1 searchers (BitCrack, VanitySearch) use:

  • A fixed-base comb computes each work-item's first key P₀ = k₀·G from a precomputed table of multiples of the base point G, with almost no point-doublings.
  • An affine batched-addition walk then produces the remaining keysPerWorkItem − 1 consecutive keys (P₀ + G, P₀ + 2G, …) with cheap point additions, sharing one modular inverse across a whole sub-batch (Montgomery's simultaneous inversion).

This is the hot path of GPU scanning, and it is EC-arithmetic-bound, not hash-bound. It is also deliberately not constant-time (speed over side-channel resistance — see the security note under Find Addresses).

🔧 Tuning the two knobs that control it — batchSizeInBits and keysPerWorkItem — is the single biggest performance decision. The defaults are conservative and not optimal for scanning. For the full mechanism, the staged optimization history with measured benchmarks, per-device tuning guidance, and how to benchmark correctly, see docs/performance.md.

📦 Batch Size per Kernel (batchSizeInBits)

Each Find-mode producer batch covers 2^batchSizeInBits consecutive private keys (the field is a log₂ exponent, not a raw key count). For each batch the CPU:

  1. Pulls a candidate private key from the configured key producer.
  2. Aligns the candidate DOWN to a 2^batchSizeInBits boundary — the low batchSizeInBits bits are cleared so the result is a multiple of 2^batchSizeInBits. This produces the secretBase. Source: KeyUtility.alignDown and AbstractProducer.createSecretBase.
  3. Submits secretBase (plus the configured keysPerWorkItem) to the GPU.

The OpenCL kernel then evaluates secretBase + i for i ∈ [0, 2^batchSizeInBits) in parallel across all GPU work-items, so the batch covers 2^batchSizeInBits consecutive keys cleanly without gap or overlap.

Typical values

batchSizeInBits Keys per batch (2^N) Use case
0 1 sequential / secrets-file mode (no batching) — used by config_Find_SecretsFile.json
14 16,384 per-CPU-producer batch — used by config_Find_8CPUProducer.json and the CPU sides of mixed configs
18 262,144 typical OpenCL device — used by config_Find_1OpenCLDevice.json
2021 1M–2M high-end OpenCL device

Upper bound: OpenClKernelConstants.BIT_COUNT_FOR_MAX_CHUNKS_ARRAY (= 24) — derived from Integer.MAX_VALUE / CHUNK_SIZE_NUM_BYTES so per-batch result arrays cannot exceed Java's 32-bit array-length limit. Default: 0 (sequential, no batching).

Relation to keysPerWorkItem

keysPerWorkItem does not change the batch size. It only changes how the same 2^batchSizeInBits work is distributed across GPU work-items:

  • With keysPerWorkItem = 1: the kernel launches 2^batchSizeInBits work-items, each computing one key.
  • With keysPerWorkItem = 8: the kernel launches 2^batchSizeInBits / 8 work-items, each computing 8 keys internally via the affine-addition scalar walker described below.

Constraints (enforced by CProducerOpenCL): keysPerWorkItem must be a power of two, 2^batchSizeInBits must be divisible by keysPerWorkItem, and the total per-launch result buffer (workSize × keysPerWorkItem × chunkSize) must not exceed Integer.MAX_VALUE.

🔄 Keys per Work-Item (keysPerWorkItem)

keysPerWorkItem controls how many consecutive keys each GPU work-item generates: it computes the first key with the fixed-base comb (P₀ = k₀·G) and the rest with the cheap affine-addition walk. Raising it amortizes the one expensive k₀·G over many cheap steps and shrinks the work-item count by the same factor — until too few work-items remain to keep the GPU busy. There is therefore a per-device sweet spot.

  • Must be a power of two; batchSizeInBits must be divisible by it.
  • The default 1 is the slowest setting (a full k·G per key) — raise it for scanning.
  • The optimum is device-dependent. On an NVIDIA RTX 3070 Laptop it is 128 (~5× faster than 1, ~22 M keys/s at batchSizeInBits=20). After the comb optimization the throughput curve is flat enough that even 1632 captures most of the gain on a wide range of GPUs (the example configs use 16).

📈 Find your device's sweet spot — and read the benchmarking caveats (laptop GPUs throttle!) — before trusting any number: see docs/performance.md. It has the per-keysPerWorkItem benchmark tables, the full optimization history with measured gains, the KEYS_BATCH_INV tuning knob, and the exact GridSizeSweepBenchmark invocation.

🚀 MSB-Zero Optimization

To accelerate elliptic curve multiplication, BitcoinAddressFinder applies a 160-bit private key optimization:

  • The upper 96 bits of each 256-bit private key are set to zero
  • Only the lower 160 bits are randomized and traversed

This reduction in scalar size speeds up k·G computations, resulting in significantly better performance during brute-force and batch-based key scanning.

✅ Matches the size of RIPEMD160(SHA256(pubkey))
✅ Especially effective when combined with OpenCL acceleration

🚀 Pluggable Address Lookup Backends (addressLookupBackend)

As of version 1.6.0, the address presence check is decoupled from the on-disk LMDB store via a small AddressPresence interface. The consumer's scan hot path queries through whichever backend is selected in the config — LMDB itself, a Bloom/Binary-Fuse filter in front of LMDB, or a self-contained in-memory snapshot that lets the LMDB env be closed and garbage-collected after population.

"addressLookupBackend": "LMDB_ONLY"

The default is LMDB_ONLY: no in-RAM filter, LMDB stays open and answers every lookup exactly. This is the safest baseline — an exact backend can never report a false positive as a hit — and the in-RAM filters below are opt-in optimisations you add only when you want them. (Note the GPU pre-filter, producerOpenCL.enableGpuFilter, is independent of this setting and works with the LMDB_ONLY default; see below.)

Which filter to pick

Most users should just run the shipped example examples/config_Find_GPU_Fuse16Cascade.json — it is already set to the recommended combination. There are two independent knobs, and the defaults are:

Knob Recommended Why
GPU pre-filter (gpuFilterType, needs enableGpuFilter) FUSE_16 raises end-to-end throughput 1.2×–7.2×; fits VRAM even at the Full DB tier
Consumer backend (addressLookupBackend) BINARY_FUSE_8 small, cheap independent second stage — or LMDB_ONLY for exact, false-positive-free checks

They form a cascade: the GPU pre-filter decides what crosses PCIe, the consumer backend re-checks the survivors. Because the two filters hash independently, the second stage rejects what the first let through — so they must be different filters. Full analysis, with all measurements, in docs/filter-selection.md and docs/performance.md.

Why FUSE_16 on the GPU, even though it is not the fastest to probe. Blocked Bloom probes 1.6–2.3× faster on the device, but the device is not the bottleneck — the single consumer thread, verifying survivors against LMDB, is. A faster probe that passes more candidates (a higher false-positive rate) just floods that thread. FUSE_16's 0.0016 % FPR hands it 256× less work than FUSE_8 and removes it as the bottleneck. Measured end-to-end against a real database, only the GPU filter varied:

Consumer threads FUSE_8 net throughput FUSE_16 net throughput Speed-up
8 188.6 M/s 227.8 M/s 1.21×
1 31.7 M/s 228.3 M/s 7.24×

The gain grows as the consumer gets tighter — fewer threads, colder storage, or several GPUs feeding one consumer — and is never negative. It costs 2× the VRAM (2.25 vs 1.13 B/entry), which fits an 8 GB card at every tier.

Deviate when:

  • CPU-only runs (no GPU): BINARY_FUSE_16 as the consumer backend. Against a cold database its 0.0016 % FPR makes verification cost vanish — the largest single effect in the whole comparison.
  • Rebuild often, or short on build RAM: BLOCKED_BLOOM (low density, bpe 9–11). It builds 3–4× faster in a single streaming pass, where Binary Fuse peels through ~29 B/entry of auxiliary arrays (~40 GB transient at the Full DB tier). At bpe ≥ 17 BINARY_FUSE_16 dominates it on both memory and accuracy. It is not offered as a GPU pre-filter on purpose — its faster probe passes more candidates to the consumer, which loses net throughput.
  • LMDB_ONLY — whenever a false positive is unacceptable rather than merely costly.

On GPU, CL_DEVICE_MAX_MEM_ALLOC_SIZE is a reported floor, not a hard cap. The filter is a single OpenCL allocation. NVIDIA reports a conservative quarter of VRAM (2047 MB on an 8 GB RTX 3070) but the driver actually honours far larger single buffers — a 3.1 GB blocked-bloom filter allocated and ran fine on the 3070 — so BINARY_FUSE_16 (3.14 GB) fits the Full DB tier there after all. The robust test is whether the allocation succeeds (the upload path fails loudly if it does not), not the reported number. Run {"command":"OpenCLInfo"} to read it.

All numbers above come from the storage-free bench in docs/measurements/bench_filters.sh, which measures the filters without LMDB behind them so the results reproduce across machines. Run it yourself; the false-positive rates are deterministic and must match to the digit.

Supported values:

Value RAM cost Lookup latency LMDB stays open? Best for
LMDB_ONLY minimal (mmap only) slowest yes default — exact, no filter, can never produce a false hit; also the simplest base for GPU pre-filtering
BLOOM ~80 MB – ~1.5 GB (FPP) fast for misses yes when you want to skip LMDB for the common miss case but keep it open to verify hits
HASHSET ~80 B / entry fast no small databases where memory is plentiful and exact lookup is required by callers
TRUNCATED_LONG_64 ~8 B / entry fast when small, degrades with size no best in-RAM trade-off when you do want to drop LMDB and the database is small; latency scales worse than any other backend (see the size sweep)
BINARY_FUSE_8 ~1.13 B / entry fast yes ultra-low RAM filter in front of LMDB; 0.4% of filter hits are verified against LMDB (also feeds the GPU pre-filter)
BINARY_FUSE_16 ~2.25 B / entry fast yes like Fuse-8 but 0.0015% FPR — fewer LMDB verifications; use when Fuse-8's verification cost is measurable
BLOCKED_BLOOM 1.375 B / entry (bpe 11, configurable) fastest probe yes rebuild-heavy workloads — constructs 3× faster and streams in one pass instead of peeling through ~29 B/entry of auxiliary arrays; loses on total cost because its FPR is ~4× higher at equal size

Memory footprint per backend across the published database tiers

HASHSET stores the full hash160; TRUNCATED_LONG_64 keeps only the 8 bytes after the bucket byte. Both in-RAM backends use the same 256-bucket layout (one chunk per first-byte value) so per-entry numbers dominate.

Backend 100 M entries (operational) 132 M entries (Light DB) 1.377 B entries (Full DB)
HASHSET ~8.0 GB ~10 GB ~110 GB ❌
TRUNCATED_LONG_64 ~0.8 GB ~1.1 GB ~11 GB
BLOOM (FPP 0.01) ~120 MB ~150 MB ~1.6 GB
BINARY_FUSE_8 ~113 MB ~149 MB ~1.5 GB
BINARY_FUSE_16 ~225 MB ~298 MB ~3.1 GB
BLOCKED_BLOOM ~138 MB ~182 MB ~1.89 GB

Every row is now exact, reported by each backend's sizeInBytes() rather than estimated from heap deltas. The old estimates ran ~25 % high (1.412 vs the true 1.126 B/entry for Fuse-8), which made them unusable for a comparison in which memory is a decision criterion.

Per-entry constants: Fuse-8 1.126 B, Fuse-16 2.252 B, Blocked Bloom 1.375 B at the default 11 bits/entry. Blocked Bloom's density is configurable via blockedBloomBitsPerEntry and blockedBloomK; the measured optimum for k is 5/6/7/7/8/9 at 8/11/14/17/21/26 bits per entry. It saturates sub-linearly, so there is no proportional rule — an earlier k ≈ 0.55 × bitsPerEntry was inferred from the low end of that sweep and overshoots by 2 at the top.

Blocked Bloom no longer rounds to power-of-two block counts. It sizes exactly via fastrange (unsignedMultiplyHigh(hash, numBlocks), mul_hi on the GPU). The old rounding wasted up to 2×: a request for 11 bits/entry at 100 M entries delivered 21.47. Per-entry cost is now a constant, not a step function that fell as the database grew.

Self-contained snapshots release the LMDB env

HASHSET and TRUNCATED_LONG_64 are replacements, not decorators: they are populated once from LMDB via a streaming pass, then the LMDB env is closed and the reference to it is dropped. The on-disk store is no longer needed and the file-backed mmap pages can be reclaimed by the OS. Whether the chain still needs LMDB is reported by the requiresBackend() method on the chosen backend.

Bucketing by the first byte

TRUNCATED_LONG_64 uses 256 buckets indexed by the first byte of each hash160. A single Java long[] is capped at Integer.MAX_VALUE elements; bucketing keeps every bucket well below that limit and gives O(1) bucket selection plus a much smaller per-bucket binary search. At the Full DB tier each bucket holds ~5.4 M entries.

Why TRUNCATED_LONG_64 is so close to HASHSET despite being a binary search

hash160 is the output of SHA-256 followed by RIPEMD-160 and is cryptographically uniform; any subset of its bits is also uniform. TRUNCATED_LONG_64 keeps only the 8 bytes after the bucket byte (72 bits of total resolution: 8-bit bucket + 64-bit stored value). The probability that a random query collides with one of N stored entries is N/2⁶⁴, which for the largest published database tier is ~7.5 × 10⁻¹¹ per query — rare enough that surfacing such a collision as an unverified hit is harmless (LMDB is closed after population, like HASHSET). Unlike the Binary Fuse backends, whose 0.4%/0.0015% FPR is far too high to leave unverified, TRUNCATED_LONG_64 needs no LMDB verifier. In return:

  • Cache-line dense: 8 longs per 64-byte cache line, the binary search walks very little memory.
  • JDK intrinsic: Arrays.binarySearch(long[], long) is heavily JIT-optimised and produces branchless comparisons on modern CPUs.
  • No boxing, no per-entry object header, no pointer chasing — every comparison is a single CPU instruction.

Binary Fuse Filters — probabilistic presence with no false negatives

BINARY_FUSE_8 and BINARY_FUSE_16 are Binary Fuse Filters (Graf & Lemire, 2022) — a faithful port of the reference FastFilter construction. Each inserted key derives a single 64-bit hash murmur64(key + seed); its high bits pick a base slot and two low/mid bit-windows offset it into three consecutive, overlapping segments. A simple XOR invariant across those three slots makes lookup a single three-read check:

hash = murmur64(key + seed)
base = unsignedMulHigh(hash, segmentCountLength)          // in [0, segmentCountLength)
h0 = base
h1 = base + 1·segmentLength  ⊕ ((hash >>> 18) & segmentLengthMask)
h2 = base + 2·segmentLength  ⊕ ( hash         & segmentLengthMask)
hit ⟺  fingerprints[h0] ⊕ fingerprints[h1] ⊕ fingerprints[h2] == fingerprint(hash)

Unlike a plain XOR filter's three disjoint segments (~1.23× space), the fused overlapping-segment geometry lets construction succeed on the first attempt at only ~1.125× space. Duplicate 64-bit keys (two distinct hash160 sharing their first 8 bytes) cannot be placed, so they are de-duplicated before construction — lossless, since every filter hit is verified against LMDB anyway.

No false negatives — every key inserted via populateFrom is always found, so a filter miss is definitive.
False-positive rate — 1/256 ≈ 0.4% for Fuse-8 (8-bit fingerprint), 1/65536 ≈ 0.0015% for Fuse-16 (16-bit fingerprint). That FPR is far too high to report a filter hit as a final hit (at 0.4% roughly one in every 250 scanned addresses would be a spurious hit), so the filter is used exactly like BLOOM: a miss short-circuits without touching LMDB, a hit falls through to LMDB to confirm the address or reject it as a false positive.
Decorator, not a replacementBinaryFuseAccelerator wraps the filter plus the LMDB delegate; requiresBackend() returns true, so the LMDB env stays open as the exact verifier (unlike the self-contained HASHSET / TRUNCATED_LONG_64 snapshots, which drop LMDB).

Choose between the two variants based on how expensive the LMDB verification of filter hits is in your workload. For most use cases with a light or full database, BINARY_FUSE_8 gives the best RAM-to-latency trade-off: a tiny in-RAM filter that skips LMDB for the ~99.6% of queries that miss, while BINARY_FUSE_16 drops the FPR ~256× (fewer LMDB verifications) at the cost of 2× the fingerprint array size.

BLOCKED_BLOOM — the filter that builds at the billion-entry tier

The Binary Fuse filters are excellent at rest (1.13 B/entry is unbeatable) but expensive to construct: peeling holds several auxiliary arrays live at once — the 64-bit keys, per-slot counters, an XOR accumulator, a peeling queue, and the reverse order/alone arrays — roughly 29 bytes per entry at peak, ≈ 42 GB at the 1.377 B-entry Full DB.

Correction. An earlier edition of this document claimed that made BINARY_FUSE_8 unbuildable at the Full DB tier on commodity hardware. That was wrong, and it was the stated reason this backend existed. Independently retested on a 61.6 GB host (cache cleared first, -Xmx48g), Fuse-8 builds in 1 564 s to 1 504 MiB with a 0.393 % FPR and no false negatives. The original observation — a build that appeared to hang — was the I/O-bound ingest stalling under memory pressure, not the peeling step: peeling itself finished in 76 s at ~19 M keys/s. Two different phenomena were conflated.

BLOCKED_BLOOM's remaining edge at that tier is purely build cost — it builds roughly twice as fast — not lookup speed (which does not reproduce across machines) and not because fuse is impossible. On total cost BINARY_FUSE_8 wins (smaller, lower FPR, verified over the database's whole life against a one-time build), so it is the recommended full-tier backend; reach for BLOCKED_BLOOM when builds are frequent or heap during the build is tight.

BLOCKED_BLOOM exists to close exactly that gap. It is a blocked Bloom filter: allocate the bit array once, then stream every address through it setting k bits. There is no peeling, no auxiliary array, no second pass — peak build memory is the filter itself (~2 GB at the Full DB tier), so it builds anywhere the finished filter fits.

Blocked layout — all k bits in one cache line. The bit array is split into 512-bit blocks (64 bytes = one CPU cache line = one coalesced GPU memory transaction). A key's block is chosen from the high bits of one hash; all k of its bit positions are then confined to that block:

key   = hash160[0..7] as big-endian long        // same extraction as the fuse filters
a     = murmur64(key)
b     = murmur64(key + GOLDEN)
block = a >>> (64 - logBlocks)                   // uniform block index
bit_i = ((int)b + i * (int)(b >>> 32)) & 511     // i = 0..k-1, all inside that block
hit  ⟺ every bit_i is set

So a lookup touches exactly one 64-byte region instead of the three scattered reads a fuse lookup makes. That is what makes the extra bit-probes nearly free once the filter no longer fits in cache — and it is the property that makes this layout the natural candidate for a future GPU-side filter (one coalesced read per candidate).

No false negatives — every inserted key always passes, so a filter miss is definitive. False-positive rate — ~0.76 % at the shipped default (k = 6, 11 bits/entry; fastrange sizes it to exactly that density at every tier). Like the fuse filters this is far too high to report unverified, so BlockedBloomAccelerator is a decorator: requiresBackend() returns true, a miss short-circuits, a hit falls through to LMDB to confirm or reject.

Measured against a real database (FilterMeasurementMain, Light DB = 132,288,304 entries; build time, retained heap after GC, and 5 M random-non-member probes; zero false negatives on 20 k sampled real members in every row):

Backend build time retained RAM B / entry lookups/s measured FPR
BLOCKED_BLOOM 34.8 s 259.5 MiB 2.057 11.54 M 0.258 %
BINARY_FUSE_8 68.8 s 143.4 MiB 1.137 12.93 M 0.390 %
BINARY_FUSE_16 70.6 s 287.4 MiB 2.278 12.30 M 0.002 %
TRUNCATED_LONG_64 41.9 s 1082.2 MiB 8.578 1.52 M 0.000 %

The BLOCKED_BLOOM row here was measured before fastrange, at the old power-of-two default (~16 bits/entry). At the current default (11 bits/entry, k = 6) it is smaller — 1.375 B/entry — but has a higher FPR of ~0.76 %; the build-time and lookup columns are representative either way.

Reading the table: BLOCKED_BLOOM builds ~2× faster than either fuse variant (streaming vs. peeling), costs somewhat more RAM at the same density, and looks up in the same ballpark — the differences are modest. Its advantage is confined to build cost and heap at the Full DB tier (a rebuild-heavy or heap-constrained niche), not the overall recommendation — which is BINARY_FUSE_8 on total cost — and not whether fuse can build at all (it can — see below).

Full DB (1.377 B entries) — where blocked Bloom's cheaper build matters

Same harness, same machine (64 GB RAM), against the 61 GB / 1,377,478,516-entry database. Blocked Bloom builds it in a 6 GB heap (it only needs the finished filter); the fuse filters build here too, but their multi-pass peeling needs ~40 GB of transient heap (-Xmx48g), so an under-sized heap OOMs mid-peel — that, not any algorithmic limit, is what an earlier version of this document mistook for "fuse cannot build at the Full DB tier". Blocked-Bloom row:

metric measured
entries 1,377,478,516
retained RAM 2,051.8 MiB (2.0 GiB)
bytes / entry 1.562
lookups/s 9.53 M
false-positive rate 0.485 %
false negatives 0
build time 1,869 s (31 min, I/O-bound — see below)

Faster to build than Fuse-8 at this tier — the lookup question is unresolved. Both backends measured on both hosts, each arm preceded by a cache-clearing pass, at -Xmx48g:

16 MB L3 host 96 MB L3 host
BINARY_FUSE_8 BLOCKED_BLOOM BINARY_FUSE_8 BLOCKED_BLOOM
build 3 186 s 1 617 s 1 564 s 859 s
lookups/s 9.37 M 7.44 M 6.13 M 7.17 M
retained 1 504 MiB 2 080 MiB 1 504 MiB 2 080 MiB
FPR 0.393 % 0.485 % 0.393 % 0.485 %

Blocked Bloom builds faster on both machines. The 1.8–2.0× seen here is an LMDB-bound figure; measured storage-free the true algorithmic factor is 3.0× (14.6 s vs 43.8 s at 100 M), because these runs spend most of their time on a scattered 61 GB cursor walk that is common to both filters and dilutes the difference. Fuse-8 is 27 % smaller with a ~19 % better FPR, both bit-identical across hosts.

The lookup row above is an artefact — do not use it. It comes from a single-shot probe loop run right after a multi-GB build, and re-measuring with the warmed, storage-free JMH instrument inverts the answer on both machines. Measured ns/op at 1 B entries:

BINARY_FUSE_8 BLOCKED_BLOOM
16 MB L3 host 91.7 79.8
96 MB L3 host 39.9 42.7

So the ranking flips with cache size: blocked Bloom leads by 13–36 % where L3 is modest, Fuse-8 by ~6 % (intervals overlapping) where it is large. Note the same Fuse-8 array costs 91.7 vs 39.9 ns on the two hosts — 2.3× from cache alone. Full analysis in docs/performance.md.

Recommendation: BINARY_FUSE_8 — see Which filter to pick. Blocked Bloom wins every probe column on this page, and still loses the total: at equal footprint its false-positive rate is ~4× higher, and a false positive costs 4.1 µs warm to 292.7 µs cold against tens of nanoseconds for the probe. The probe is the term that barely counts. Blocked Bloom remains the pick when build time dominates (3× faster, one streaming pass, no ~29 B/entry peeling arrays).

Build time is dominated by LMDB I/O, not by the filter, and the lever is free RAM. The database is written in random hash160 order, so a key-ordered cursor walk is physically scattered across the 61 GB file; with headroom the pages accumulate, without it they are evicted before reuse and re-read. Measured mid-build under memory pressure: the NVMe ran 86 % busy at 332 MB/s while yielding only ~19 MB/s of useful entry data — roughly 17× read amplification. The same build took 1,869 s with ~2 GB free but 1,043 s starting from an emptied cache with 58 GB free — nearly 2× faster from the colder start.

Setting "useNoReadAhead": true opens LMDB with MDB_NORDAHEAD, asking the OS not to prefetch neighbours that get evicted before use. This flag has no effect on Windows — LMDB does not implement it there (see the MDB_NORDAHEAD javadoc in lmdbjava: "The option is not implemented on Windows"), so it is a POSIX-only lever. On Linux it is the standard remedy for the amplification described above; it is currently unvalidated in this project, because the only measurements taken so far were on Windows where it does nothing. It defaults to false. See docs/performance.md for the measurement history and TODO.md for the remaining levers (higher queue depth, key-order compaction).

Why k = 6 — the measured sweep, and how the optimum shifts with density

k (bits set per key) is the accuracy knob. Too few and the filter is too permissive; too many and the shared 512-bit block saturates, raising the FPR again. The optimum is not guessed — it is measured. Since fastrange replaced power-of-two block rounding, the density is exactly what you configure, and the shipped default is DEFAULT_K = 6 at DEFAULT_BITS_PER_ENTRY = 11. At that default density (10 M synthetic entries, 2 M random-non-member probes per row):

k 4 5 6 7 8 10
FPR @ 11 b/e 0.997 % 0.805 % 0.758 % 0.764 % 0.806 % 0.996 %

More bits is not monotonically better — the FPR bottoms out at k = 6 and climbs again, because every additional probe fills the same 512-bit block faster. And the blocked optimum sits below the textbook unblocked prediction (m/n)·ln2 ≈ 7.6 for 11 b/e: confining all probes to one block introduces per-block load variance, which penalises high k. So DEFAULT_K is an empirical result, not a formula.

The optimum is density-dependent and must be raised together with bitsPerEntry. Measured across the whole surface (10 M entries, fastrange sizing, so the effective density equals the configured one exactly):

bits/entry 8 11 (default) 14 17 21 26
optimum k 5 6 7 7 8 9
FPR at optimum 2.44 % 0.758 % 0.306 % 0.153 % 0.086 % 0.055 %

The optimum saturates sub-linearly: the ratio k / bits-per-entry falls from 0.63 at 8 b/e to 0.35 at 26 b/e, so there is no proportional rule. (An earlier k ≈ 0.55 × bits/entry was inferred from the middle of this range — it happens to fit near 11 b/e but overshoots by 2 at the top.) If you raise blockedBloomBitsPerEntry, raise blockedBloomK to the matching column, or you measure a mismatched geometry rather than a denser filter.

k = 1 would be a direct-addressed bitmap — one bit per hash, no shared block, fastest of all — but its FPR is 1 − e^(−n/m) ≈ 6 % at these densities, so roughly 1 query in 17 falls through to LMDB. That verification cost dwarfs any lookup-speed saving, which is why the default is a multi-probe k, not 1.

Size (density) and lookup speed

Because containsAddress short-circuits on the first unset bit, a sparser (lower-density, lower-k) filter answers a miss sooner — lookup speed tracks sparsity, not size. That is the opposite of intuition and matters when tuning: a denser blocked-bloom filter buys a lower FPR but each probe is slightly slower, and the two effects trade off through the cost model (§ docs/filter-selection.md).

This is also the clearest statement of why blocked Bloom is an addition, not a replacement for the fuse filters: at equal footprint the fuse construction always has the lower FPR (e.g. at ~1.13 B/entry, Fuse-8's 0.39 % against blocked bloom's 1.6 %; at ~2.25 B/entry, Fuse-16's 0.0016 % against 0.13 %). Per byte stored, fuse is simply the better filter — blocked bloom's justification is its ~3–4× faster single-pass build and its faster GPU probe, not accuracy per byte.

Probe stride must be odd

The probe sequence is bit_i = (x + i·y) mod 512, whose period is 512 / gcd(y, 512). With an unconstrained stride y, 1 key in 512 draws y ≡ 0 (mod 512) and places all k probes on a single bit; 1 in 256 reaches only 2 distinct bits, 1 in 128 only 4. Those degenerate keys are near-useless filter entries and measurably inflate the aggregate FPR — 0.258 % before the fix versus 0.184 % after, at identical size and k. Forcing y odd makes gcd(y, 512) = 1, so the probes are always distinct. Cost: one OR instruction. BlockedBloomAddressPresenceTest#probePositions_areAlwaysDistinct_becauseStrideIsOdd fails if it is ever dropped.

Which consumer backend to choose (addressLookupBackend; the GPU pre-filter is a separate knob — see Which filter to pick):

  • Small/medium databases (below ~15 M entries) → BINARY_FUSE_8. Fastest filter measured at that scale on either machine, and lowest RAM.
  • Large databases → BINARY_FUSE_8, on either host. Cache size changes which filter probes faster, but not which one is cheaper overall: verification cost dominates, and Fuse-8's FPR is ~4× lower at equal footprint. Against genuinely cold storage BINARY_FUSE_16 pulls ahead (its FPR is two orders of magnitude lower). Budget ~40 GB of transient heap to build Fuse at the 1.377 B tier (the peeling arrays need ~29 B/entry); BLOCKED_BLOOM needs only the finished 1.89 GB and builds 3× faster, so prefer it if you rebuild often or cannot spare the heap.

Ready-to-run example covering both tiers, on GPU: examples/config_Find_GPU_Fuse16Cascade.jsonFUSE_16 GPU pre-filter + BINARY_FUSE_8 consumer backend, the recommended cascade (see Which filter to pick).

For the consumer backend specifically, BINARY_FUSE_8 is the safe simple default on both tiers; BINARY_FUSE_16 is the better consumer choice against cold storage, where its far lower FPR makes verification cost vanish. Both fuse widths now have a GPU kernel. Building Fuse at the Full DB tier needs roughly 40 GB of transient heap for the peeling arrays, so raise -Xmx accordingly; if that heap is unavailable or you rebuild often, BLOCKED_BLOOM builds 3–4× faster in a single streaming pass and needs only its finished 1.89 GB.

BLOCKED_BLOOM is therefore an addition, not a replacement — the two filters have genuinely different optimal domains, so neither was removed.

Benchmark numbers

AddressLookupBenchmark (JMH, replaces the old JUnit timing test) writes 2048 random hash160 entries into LMDB, then measures average containsAddress time per backend across 8192 pre-generated miss queries with JMH-driven JIT warmup and fork isolation. Sample numbers from one workstation run — relative ordering is the architecturally interesting part; absolute values vary by machine, JVM, and dataset size:

Backend ns / op
LMDB_ONLY 833
BLOOM 155
HASHSET 42
TRUNCATED_LONG_64 40
BINARY_FUSE_8 ~19
BINARY_FUSE_16 ~23
BLOCKED_BLOOM ~30

This table is the cache-hot regime and does not generalise — see the scale sweep below. With only 2048 entries every filter fits in L1, so the measurement reduces to instruction count. The ranking changes substantially at real database sizes.

Lookup latency vs database size (FilterLookupBenchmark)

The 2048-entry table above measures one point on a curve, and it is the least representative one. FilterLookupBenchmark sweeps the entry count instead, and removes LMDB entirely — addresses come from a seeded PRNG (PrngAddressIterable), so there is no page-cache behaviour, no read amplification, and no run-to-run variance from what the OS happened to cache. Only the filter is measured. Average ns per containsAddress against non-member probes:

Backend 100 K 1 M 10 M 50 M 100 M
BINARY_FUSE_8 13.1 18.5 31.2 52.1 59.3
BINARY_FUSE_16 15.1 19.2 36.5 60.9 65.8
BLOCKED_BLOOM 23.9 33.2 39.8 40.5 50.2
HASHSET 52.1 92.9 145.1
TRUNCATED_LONG_64 80.1 145.4 337.3 533.3 635.5
BLOOM 85.4 98.9 114.1 203.2 203.8

HASHSET is omitted above 10 M — at ~80 B/entry it needs ~8 GB at 100 M.

There is a crossover — but it is set by L3 cache size, not by the filters. On this reference machine (16 MB L3) it sits between 10 M and 50 M entries: below it BINARY_FUSE_8 wins by ~1.7×; above it BLOCKED_BLOOM wins by ~1.3×. The mechanism is visible in the step sizes: from 10 M to 50 M Fuse-8 degrades by ~1.7× (31.2 → 52.1 ns) while blocked Bloom barely moves (39.8 → 40.5 ns, 1.02×). That is exactly where the Fuse-8 array outgrows L3 cache (~11 MB at 10 M, ~57 MB at 50 M). Once neither filter is cache-resident, a fuse lookup pays three scattered cache misses while a blocked-Bloom lookup pays one — all k probes share a single 512-bit block. Blocked Bloom is the flattest structure in the table (1.9× from 100 K to 100 M, against Fuse-8's 4.5×).

Practical reading: this crossover moves with the cache, so "blocked Bloom wins at production scale" is machine-specific, not universal. On a large-cache host (96 MB L3) Fuse-8 stays ahead through 100 M+, and even at the Light DB tier (132 M) it measures faster (32.8 vs 37.1 ns). The lookup ranking is close and cache-dependent, which is why the recommendation does not rest on it but on total cost (size + FPR + a one-time build, amortised over the database's life) → BINARY_FUSE_8. See docs/performance.md for the second-machine confirmation and the full 500 M / 1 B columns.

  • TRUNCATED_LONG_64 degrades worst of all (7.6× from 100 K to 100 M, ending at 612 ns — 13× slower than blocked Bloom). Its binary search costs ~log₂(n/256) dependent cache misses. The "near-HASHSET latency" characterisation holds only for small databases.
  • BLOOM is the slowest filter despite scaling flatly (2.4×). See the tuning history below; what remains is structural in Guava.

Run it yourself:

# all backends at one size
java ... org.openjdk.jmh.Main FilterLookupBenchmark -p entries=1000000 -f 1 -wi 3 -i 5

# scale sweep, filters only (HASHSET needs ~80 B/entry, so give it heap or omit it)
java ... org.openjdk.jmh.Main FilterLookupBenchmark \
    -p backend=BLOOM,BINARY_FUSE_8,BINARY_FUSE_16,BLOCKED_BLOOM \
    -p entries=1000000,10000000,50000000 -f 1 -wi 3 -i 5

The default backend is LMDB_ONLY (no in-RAM structure, exact, can never produce a false hit). When you do want to trade RAM for speed, TRUNCATED_LONG_64 reaches near-HASHSET latency at ~10× lower memory cost on small databases — but see the size sweep above: its binary search degrades faster than any other backend, and by 10 M entries it is the slowest option measured. BINARY_FUSE_8 and BINARY_FUSE_16 offer even lower memory with O(1) lookup (3 direct array reads + 1 hash) but keep LMDB open to verify hits; cache-cold latency grows with filter size, but for small-to-medium databases the three-read pattern typically stays in L2 or L3 cache.

Run the backend comparison benchmark yourself with:

mvn test-compile exec:java -Dexec.args="AddressLookupBenchmark"

# Narrow to a single backend
mvn test-compile exec:java \
  -Dexec.args="AddressLookupBenchmark -p backend=BINARY_FUSE_8"

# Allocation profile (per-op bytes allocated)
mvn test-compile exec:java -Dexec.args="AddressLookupBenchmark -prof gc"

For an isolated view of Binary Fuse Filter construction and lookup without LMDB (L1/L2/L3 cache-size sweep):

mvn test-compile exec:java -Dexec.args="BinaryFuseBenchmark"

GPU-side Binary Fuse 8 filtering (enableGpuFilter)

By default the OpenCL kernel transfers every derived work-item result back to the host, and the consumer then checks each hash160 against the selected addressLookupBackend. With enableGpuFilter, a Binary Fuse 8 filter is instead pushed onto the GPU so the kernel checks each derived hash160 inline and transmits only the candidates the filter marks as possibly present. This collapses the PCIe read-back from the full 2^batchSizeInBits grid down to the handful of hits per batch — the dominant cost on large batches.

The GPU pre-filter is independent of the CPU addressLookupBackend. It is built once during consumer init — while LMDB is still open, before a self-contained backend (HASHSET/TRUNCATED_LONG_64) would close it — purely as a transient VRAM-upload artifact (the host copy is freed after the one-time upload). So it works with every backend, including the LMDB_ONLY default; you do not have to set the CPU backend to BINARY_FUSE_8. The survivors that come back from the GPU are verified directly against the CPU lookup, so they are never "double filtered". (If the CPU backend already is BINARY_FUSE_8, that filter is reused for the upload to avoid a second LMDB scan.)

Enable it on an OpenCL producer (see examples/config_Find_GPUFilterCompact.json):

"consumerJava": {
  "lmdbConfigurationReadOnly": { "addressLookupBackend": "LMDB_ONLY" }
},
"producerOpenCL": [
  { "enableGpuFilter": true, "transferAll": false }
]
Field Default Effect
enableGpuFilter false When true, a Binary Fuse 8 filter is built from LMDB during consumer init and uploaded to GPU VRAM (once per session); the kernel filters inline (compact output). Independent of the CPU addressLookupBackend — built while LMDB is open, so it works with every backend including the LMDB_ONLY default.
transferAll false Forces full-transfer (legacy) output even when enableGpuFilter is on. Finder sets this to true automatically — with a warning — whenever enableVanity = true, because vanity scanning must see every derived address.

The two flags are independent, not opposites — a common point of confusion. enableGpuFilter decides whether the filter runs on the GPU; transferAll decides which output layout the kernel uses (send back all derived results, or only the filtered hits). transferAll is really a "force full-transfer" override, so enableGpuFilter: true, transferAll: false is the normal compact-filtering case — filter on, send back only the hits. How they combine:

enableGpuFilter transferAll Behaviour
true false Compact mode — filter on the GPU, only the matching candidates are transferred back (the recommended, fastest setup).
true true Filter is uploaded but overridden to full transfer — every derived result is sent back (e.g. vanity scanning, or a device without OpenCL 2.0+ compact support).
false (any) No GPU filter — full transfer (legacy); the consumer checks every result against addressLookupBackend on the CPU.

Compact mode runs only when a filter was uploaded and transferAll is false (OpenCLContext computes transferAll = producerOpenCL.transferAll || !gpuFilterUploaded).

How compact mode works — the kernel checks both the compressed and uncompressed hash160 of each candidate against the uploaded filter; a work-item that hits claims an output slot via an atomic_add on a shared count word and writes its entry there, so the buffer holds exactly the hits (each entry carries its own work-item index for secret-key reconstruction). A non-hit writes nothing.

No false negatives — the Binary Fuse 8 guarantee carries to the GPU unchanged: any address actually in the database is always flagged. The GPU pre-filter only shrinks the GPU→CPU transfer; the ~0.4 % false positives among the survivors are verified against LMDB on the CPU exactly as for the in-RAM BINARY_FUSE_8 backend (LMDB stays open as the verifier), so no false positive is ever reported as a hit.

Device requirements — compact mode needs an OpenCL 2.0+ device (the atomic_add on global memory) and a little-endian device (the kernel canonicalises its output as little-endian). Both are checked at producer init and fail fast with a clear message; on an older device set transferAll: true to keep the full-transfer path. The GPU/CPU lookup parity (key extraction + hash formula) is pinned by Fuse8GpuHashParityTest and exercised end-to-end on a real OpenCL device by OpenCLCompactOutputIntegrationTest.

Measured speedup — see GPU Binary Fuse 8 filter — compact output vs. full transfer under Performance Benchmarks for an A/B measurement (~2.2× at grid 19 on an RTX 3070).

Bloom filter — FPP tuning

When addressLookupBackend is BLOOM, the false positive probability is configurable via bloomFilterFpp:

"addressLookupBackend": "BLOOM",
"bloomFilterFpp": 0.1
bloomFilterFpp Behaviour
0.01 ✅ Only ~1% false positives — high accuracy, more memory
0.05 ⚖️ Balanced tradeoff between memory and performance
0.10.2 🪶 Very memory-efficient — suitable if some false positives are acceptable

Memory cost (Bloom only — HASHSET and TRUNCATED_LONG_64 follow the RAM cost column of the backend table above):

bloomFilterFpp Light Database (~132M) Full Database (~1.37B)
0.1 ~80 MB ~800 MB
0.05 ~100 MB ~1024 MB
0.01 ~151 MB ~1574 MB

🔐 Public Key Hashing on GPU (SHA-256 + RIPEMD-160)

The OpenCL kernel performs blazing fast public key hashing directly on the GPU using:

  • SHA-256(pubkey.x || pubkey.y) followed by
  • RIPEMD-160(SHA-256(pubkey))

This allows each GPU thread to independently generate full Bitcoin-style public key hashes (hash160) without CPU involvement.

Benefits:

  • No host-side post-processing needed
  • Fully parallelized and memory-efficient
  • Ideal for massive batch generation and filtering

✅ Output is already in hash160 format, ready for address comparison

Address Database

The addresses are stored in a high-performance database: LMDB. The database can be used to check whether a generated address has ever been used.

Address Import Support

The importer supports reading multiple .txt or .text files, each containing one address per line in arbitrary order. Lines may vary in address type and format. Each line may also optionally include an associated coin amount.

⚠️ Unsupported or malformed lines are silently skipped during import.

Supported Address Types

  • P2PKH – Pay to Public Key Hash Encoded using Base58. Most commonly used format for legacy Bitcoin addresses.

  • P2SH – Pay to Script Hash Base58-encoded address type used for multisig and other script-based spending conditions.

  • P2WPKH – Pay to Witness Public Key Hash Native SegWit v0 address, encoded in Bech32. One private key deterministically produces one address.

Unsupported Address Types

  • P2MS – Pay to Multisig Custom format prefixed with d-, m-, or s- (e.g. m-<script>). ❌ Not Supported: Script-based format. Private keys cannot deterministically generate P2MS addresses without knowing the script.

  • P2WSH – Pay to Witness Script Hash Native SegWit v0 address for scripts, encoded in Bech32. ❌ Not Supported: Script Hash format. Addresses depend on the script hash, not directly on the public key.

  • P2TR – Pay to Taproot Native SegWit v1 (Taproot) address, encoded using Bech32m. ❌ Not Supported: Requires key tweaking based on an optional script. Without knowing the script, you cannot generate matching P2TR addresses from a private key. One private key can create infinite different P2TR addresses. This breaks the tool's design, which maps private keys to deterministic addresses.

Note on Witness Versions: Bech32 was introduced in BIP-173 for SegWit v0 (P2WPKH, P2WSH). Bech32m, defined in BIP-350, is used for SegWit v1 (P2TR).

Special Formats

In addition to standard script types, the importer also recognizes and supports several special address encodings:

  • Bitcoin Cash (Base58 prefix q)
    Legacy Bitcoin Cash addresses using the q-prefix are automatically converted to legacy Bitcoin format.

  • BitCore WKH
    Custom format prefixed with wkh_ and encoded in Base36.

  • Riecoin P2SH as ScriptPubKey
    Riecoin P2SH addresses are provided as raw ScriptPubKey hex (starting with 76a914...).


Legend:
✅ = Supported and tested
❌ = Explicitly not supported (format known but intentionally excluded)
empty = Unknown or unverified — either not implemented by the altcoin project, or no valid address example was found (thus no implementation or test exists)

Coin TAG P2PKH P2SH P2WPKH P2WSH P2MS P2TR
42-coin 42
Alias ALIAS
Argoneum AGM
Artbyte ABY
Auroracoin AUR
B3Coin B3
BBQCoin BQC
Bean Cash BEAN
Biblepay BBP
BitBay BAY
BitBlocks BBK
Bitcoin BTC
Bitcoin Cash BCH
Bitcoin Gold BTG
Bitcoin Oil BTCO
Bitcoin Plus XBC
BitCore BTX
Bitmark BTMK
BlackCoin BLK
BlakeBitcoin BBTC
Blakecoin BLC
Blocknet BLOCK
BolivarCoin BOLI
BYTZ BYTZ
Canada-eCoin CDN
Catcoin CAT
ChessCoin 0.32% CHESS
Clam CLAM
CloakCoin CLOAK
C-Note CNOTE
Coino CNO
ColossusXT COLX
Compound COMP
CROWN CRW
Cryptoshares SHARES
Curecoin CURE
Dash DASH
Defcon DEFCON
DeFiChain DFI
Deutsche eMark DEM
Diamond DMD
DiminutiveCoin DIMI
DigiByte DGB
DigitalCoin DGC
Dimecoin DIME
Divicoin DIVI
Dogecoin DOGE
Dogmcoin DOGM
Doichain DOI
e-Gulden EFL
Electron ELT
Element (HYP) HYP
Elite 1337
Emerald EMD
EverGreenCoin EGC
Feathercoin FTC
Firo FIRO
Freedomcoin FREED
GapCoin GAP
Goldcash GOLD
GoldCoin GLC
Groestlcoin GRS
Hemis HMS
Herencia HEIRS
HoboNickels HBN
HTMLCOIN HTML
I/O Coin IOC
IDChain DCT
Innova INN
InfiniLooP IL8P
Infinitecoin IFC
iXcoin IXC
Kobocoin KOBO
Komodo KMD
Lanacoin LANA
Litecoin LTC
Litecoin Cash LCC
LiteDoge LDOGE
Lithium LIT
Luckycoin LKY
Lynx LYNX
MasterNoder2 MN2
Mooncoin MOON
MotaCoin MOTA
Myriad XMY
Namecoin NMC
NewYorkCoin NYC
Novacoin NVC
Oduwacoin OWC
PAC Protocol PAC
PakCoin PAK
PandaCoin PND
Particl PART
PeepCoin PCN
Peercoin PPC
Photon PHO
Pinkcoin PINK
PIVX PIVX
PotCoin POT
Primecoin XPM
PutinCoin v2 PUT
Quark QRK
Raptoreum Core RTM
Reddcoin RDD
Riecoin RIC
SaluS SLS
SexCoin SXC
Slimcoin SLM
Smileycoin SMLY
Smoke SMOKE
SpaceXpanse ROD
Sparks SPK
Stakecoin STK
Sterlingcoin SLG
Stronghands SHND
Syscoin SYS
TajCoin TAJ
Terracoin TRC
TheHolyRogerCoin ROGER
Trezarcoin TZC
Trollcoin TROLL
UFO UFO
UFOhub UFOHUB
Unitus UIS
UniversalMolecule UMO
Unobtanium UNO
Validity VAL
Vanillacash XVC
VeriCoin VRC
Versacoin VCN
Vertcoin VTC
VirtaCoin VTA
VirtacoinPlus XVP
WorldCoin WDC
Zetacoin ZET
ZCash ZEC

Create the database by yourself

Useful txt/text file provider:

Import one or more plaintext address files into an LMDB database with the AddressFilesToLMDB command (see examples/config_AddressFilesToLMDB.json and the run_AddressFilesToLMDB launchers). Files are read one at a time; a single buffered reader streams their lines to threads parser workers, and one writer stores the parsed addresses in LMDB in batches. The performance-relevant settings on addressFilesToLMDB:

Setting Default Purpose
threads 1 Parser threads. 1 keeps the exact deterministic import order; ≥2 parses in parallel — order-independent when useStaticAmount = true, otherwise a warning is logged.
writeBatchSize 32768 Addresses written per LMDB transaction.
queueCapacity 200000 Capacity of each internal hand-off queue (back-pressure vs. memory).

The single LMDB writer is the throughput ceiling (LMDB permits one writer at a time), so a handful of parser threads is enough to keep it fed.

Optional — compact the finished database. After the import you can write a smaller, read-denser copy with the CompactLMDB command (LMDB MDB_CP_COMPACT; see examples/config_CompactLMDB.json). It opens the source read-only, writes data.mdb into a separate target directory, and verifies the copy's entry count against the source. The read benefit is largest when the database exceeds RAM (a smaller file raises the OS page-cache hit rate); for a database that fits in RAM the gain is marginal.

Optional — extract what a newer database adds over a reference. The LMDBDelta command (see examples/config_LMDBDelta.json and the run_LMDBDelta launchers) diffs one or more lmdbConfigurationReadOnlyList databases against a single referenceLmdbConfigurationReadOnly and writes every address present in the others but not in the reference to deltaAddressesFile — one mainnet Base58 1… address per line, ready to be re-imported with AddressFilesToLMDB. Because LMDB stores keys sorted, all databases are streamed together as a k-way cursor merge: memory stays flat regardless of the delta size (only one 20-byte key per cursor is held), cross-database duplicates are removed, and the whole pass is sequential I/O rather than random lookups. The re-encoding keys on the 20-byte hash160, so the round trip is lossless for lookups even though a hash that originally came from a P2SH/bech32/altcoin address is written in 1… form.

Export

The exporter provides various output formats for Bitcoin and altcoin address data:

  • HexHash
    Exports addresses as raw hash160 (RIPEMD-160 of the SHA-256 of the public key), encoded in hexadecimal. No version byte or checksum is included. Ideal for advanced usage and low-level comparison. Best viewed in fixed-width hex viewers (e.g., HxD).

  • FixedWidthBase58BitcoinAddress
    Exports Base58Check-encoded addresses (e.g., legacy P2PKH) in a fixed-width format for consistent alignment. No amount is included. Suitable for hex/byte-aligned visual inspection and batch comparison.

  • DynamicWidthBase58BitcoinAddressWithAmount
    Exports Base58Check-encoded addresses along with their associated amounts (e.g., balance or UTXO value), using a dynamic-width format. Suitable for human-readable CSV-like formats and analytics.


Use My Prepared Database

I am in the process of building and publishing databases containing large sets of Bitcoin and altcoin addresses.
(Refer to the Import section above for details on supported address formats.)

The sources of these addresses are confidential, but you are fully permitted to extract, inspect, and use them.

You are also welcome to extend this database by importing your own address data. This allows you to build upon the existing dataset, tailoring it to your specific needs (e.g., adding additional coins, formats, or private address collections).

If you're missing any information or have questions about usage or content, feel free to ask or open an issue.

Light database

💡 Hint: For the light database the recommended lookup backend is BINARY_FUSE_8:

"addressLookupBackend" : "BINARY_FUSE_8"

See Pluggable Address Lookup Backends above for the full trade-off matrix and benchmark numbers. BINARY_FUSE_8 is a tiny in-RAM filter (~149 MB for the Light DB, ~0.4% false-positive rate, no false negatives) in front of LMDB: it answers almost every miss straight from RAM and keeps LMDB open only to verify the ~0.4% of filter hits, so a false positive is never reported as a hit. If you run on a GPU, pair it with the FUSE_16 GPU pre-filter (producerOpenCL.gpuFilterType) — the two form an independent cascade (see below). Want lower fallthrough for a CPU-only run? BINARY_FUSE_16 (~298 MB, ~0.0015% FPR) verifies against LMDB far less often. If you need LMDB fully closed (a self-contained in-RAM snapshot) and can spare the memory, HASHSET (~10 GB) is exact. Avoid TRUNCATED_LONG_64 here: at the Light DB's 141 M entries its lookup latency is the worst of all backends while still costing ~7× the RAM of BINARY_FUSE_8.

Checksums lmdb_light.zip
lmdb_light.zip	CRC32	85D5B66B
lmdb_light.zip	MD5	85D99D42307FAE3D2DE9842E1BC2E05C
lmdb_light.zip	RipeMD160	63F9A232B474FBDBE0BF44424848204F96D815F9
lmdb_light.zip	SHA-1	6C70EB6C9DA6E856F6AA37DE8005CDE63C73C40E
lmdb_light.zip	SHA-256	8CF75775992A3CF6DF7E92BE8BF11512865B64355AE8B9B4AC4AA17EA0D23099
lmdb_light.zip	SHA-512	7F5DC94BECA62F75ECDC3B2982F2E049B65BA1999A565184EC037F86455EE3A84A7CFF2CB5417EB897D98784BF5928376CD328E4428AEB53B546B73737DA707B
lmdb_light.zip	SHA3-224	7828FF892A2C3A132A31097B3E474960B9724F11D89E417CC41C262F
lmdb_light.zip	SHA3-256	7EAF32276D0E04199D10694DD204FBC2FD35594E537A2CFA5C5BCA6DEF3B19FB
lmdb_light.zip	SHA3-384	7DEE61D19C04734AEBD808AFF2D9B3AD14A0659D3CDB00E4E6E4324CA7317F1C48A6B32E7D02DBE0E68A67F843F2018D
lmdb_light.zip	SHA3-512	FD9F17E3ECAD1896A984BE5FA2B884184608DF2CDAA35E7465EE4DD5735FDD2E77F361EDFA563AC8754965375C68B140208C8FEFCDE18932897B929CC707BC2E
Checksums LMDBToAddressFile_Light_HexHash.zip
LMDBToAddressFile_Light_HexHash.zip	CRC32	2F79BEB6
LMDBToAddressFile_Light_HexHash.zip	MD5	42760355C2EFBCD964152F333D49083D
LMDBToAddressFile_Light_HexHash.zip	RipeMD160	6E089BC89F87F6FE9D928CA7A82CB8276E76196D
LMDBToAddressFile_Light_HexHash.zip	SHA-1	872D5EF8A90EB7B0EEC44A4D7A0284D886CAE3D5
LMDBToAddressFile_Light_HexHash.zip	SHA-256	9079152EB750D640E469A1C050FA27D982CFC2C08F519922468061834776759E
LMDBToAddressFile_Light_HexHash.zip	SHA-512	9A5298A8391F317B4593E36EB5111CAC6FF3BC23582C8A15A1052D2EE95EFA2A9F590C1BD6F28234AD1B6163BB98CD62939928F9DC20C3AF4D320D18B332987C
LMDBToAddressFile_Light_HexHash.zip	SHA3-224	45B3DED0DEFCEB7A828FDB8D7A67FA86456C8369A191B0E2E4C5A18C
LMDBToAddressFile_Light_HexHash.zip	SHA3-256	7CCCD74E911B030DBD84691E6C5253EC460B10ECC1F9327CB985276FFC36AC30
LMDBToAddressFile_Light_HexHash.zip	SHA3-384	CDEE253DF906C04FC03C4B725A123FF01AF3AAF251C4633008D3D67F5D7D567703E82F093FA085014D4A36C48EEA604F
LMDBToAddressFile_Light_HexHash.zip	SHA3-512	1C66472C3605FCB6833163B9FBDF81A6581F012A1FE0D6236C4804909AFD6E0CF15DB11FD53EFD507C8AF7CEDFA0F06EDD03FE2F84C7ACD38058604FF304EF66

Full database

  • Full (64.0 GiB), Last update: July 20, 2026

⚠️ Backend choice for the full database. At 1.377 B entries, HASHSET requires roughly ~110 GB of RAM — impractical on almost any consumer hardware. Pick one of the realistic options instead:

"addressLookupBackend" : "BINARY_FUSE_8"
Choice RAM needed at Full DB LMDB stays open? When to pick
BINARY_FUSE_8 ~1.5 GB yes recommended — tiny filter in front of LMDB; keeps LMDB open to verify the 0.4% of filter hits; also pairs with the FUSE_16 GPU pre-filter
BINARY_FUSE_16 ~3.1 GB yes recommended for CPU-only cold-storage runs — 0.0015% FPR means far fewer LMDB verifications
BLOOM (FPP 0.01) ~1.6 GB yes keeps LMDB open as the verifier; skips it for the overwhelming majority of misses
LMDB_ONLY minimal (mmap only) yes fallback when there is essentially no spare RAM and disk-backed mmap is acceptable
TRUNCATED_LONG_64 ~11 GB no not recommended — worst lookup latency at this size (see backend section); use only if you specifically need LMDB fully closed and can spare the RAM

Do not select HASHSET for the full database. See Pluggable Address Lookup Backends above for the full trade-off matrix and per-tier memory footprint.

Checksums lmdb_full.zip
lmdb_full.zip	CRC32	731194D3
lmdb_full.zip	MD5	C7A144E1474E537B58DE91D8FE9BA762
lmdb_full.zip	RipeMD160	ED6964A1722BB453D1CA97380BEEE3CA9DC495E5
lmdb_full.zip	SHA-1	8EFCE18E7173AD2ABE8D0046D5E703A337939E1E
lmdb_full.zip	SHA-256	0CDF5F126BD090A18423B3AE47B34DC84506E88A5F882DF364BEFC3F68025EB0
lmdb_full.zip	SHA-512	402BBD2BD8ADAF7837851DB8C9E9C2B9D7C8F54FCB29F4C9E7F9A021D12988D36069549CE586D015B53011E5E00DFD9EAAB9506A0659C21F8DB4C1F66A0F000D
lmdb_full.zip	SHA3-224	BF2B576F66109EBD4F0C1A67E68EAC567E106186949F9E4E5B10DCBB
lmdb_full.zip	SHA3-256	F2A64AD5F5241B30FE4C6C4DDE5DC8F4151F0BEA00AB9A015F2B5A2E90717BAD
lmdb_full.zip	SHA3-384	4D0F0E86610FBF6720AA47E3E0A55C0276AE8A7C44CAFB9B67117314518FB17248951F855DDA324CB40232CE9832B335
lmdb_full.zip	SHA3-512	EA14474708CDEF4A2422961077F066FB853247734CF1D782C5AC66F5CFCBADAB6AC32B047575501ED22490912AE6E4DA1C77BE79EF1965B0D77982F5ABE54B7E
Checksums LMDBToAddressFile_Full_HexHash.zip
LMDBToAddressFile_Full_HexHash.zip	CRC32	B2FB544F
LMDBToAddressFile_Full_HexHash.zip	MD5	439B5202737C1B5D8766E3096191411B
LMDBToAddressFile_Full_HexHash.zip	RipeMD160	77DED76F7C9CFFB103CD94B129FE9DEF0A5445F0
LMDBToAddressFile_Full_HexHash.zip	SHA-1	581067DB3C986691CEE90A315BB3668FB2643F0A
LMDBToAddressFile_Full_HexHash.zip	SHA-256	A4AAE52539F457B8551AF8E29324297029D7A3F760F185C028DB1D741E458585
LMDBToAddressFile_Full_HexHash.zip	SHA-512	090E3289BBFBAEB8F68C6EBBCE0C03843BAE389D4D4EBD178881EF11AB39C8035A726974003A01B83F15A839F0C48131353E92266F24E625F534288D2AE2682A
LMDBToAddressFile_Full_HexHash.zip	SHA3-224	C4775299678F61E000F7AE11EDB0BBF1FF142797BBA31BC95E1290D2
LMDBToAddressFile_Full_HexHash.zip	SHA3-256	5C563C323850638905FC1DBBB186FABF47F6EBF473ED50C5C5852A63EB9B9B3B
LMDBToAddressFile_Full_HexHash.zip	SHA3-384	B7561CAF76447F75813DDDE9D07D85CE5FDA8E7207C36066F79B5D105340A63AC42A22BEAF1AF9D01146202B404310CD
LMDBToAddressFile_Full_HexHash.zip	SHA3-512	6ED622047AF04BBECF46BC2E50E9169ADC2DD0B7D35E58F6A4F7B33D64AB4C1C8F91CACF89F12A87C7AD5BAC2974BFA41100550E8A8D4A2298C3A5FFB012B8AB

Pages and projects to get lists (dumps) of PubkeyHash addresses

Find Addresses

🔢 Key Generation Configuration

BitcoinAddressFinder supports multiple pseudorandom number generators (PRNGs) for private-key creation. Pick one in your JSON via the randomAlgorithm field, e.g. "randomAlgorithm": "SECURE_RANDOM". This flexibility lets you switch between production-grade entropy and deterministic or deliberately weak sources for audits and research.

Key producer Use Cases

  • 🔐 Secure wallet generation
  • 🧪 Testing deterministic output
  • 🕵️ Simulating vulnerabilities
  • 🔄 Reproducible scans

⚠️ Security Warning: This software is intended for research and educational purposes. Do not use it in production or on systems connected to the internet.

A secure environment should be fully isolated — ideally an air-gapped computer (physically disconnected from all networks).
The software is highly optimized for performance and not designed to run in constant time, which means it may be vulnerable to side-channel attacks on shared or exposed systems.

For generated vanity addresses or private keys, consider storing them safely using a paper wallet.

Supported PRNG Modes (key producer java random)

Value Description
SECURE_RANDOM ✅ Cryptographically secure system CSPRNG (/dev/urandom, Windows CNG). Recommended for real wallet generation.
RANDOM_CURRENT_TIME_MILLIS_SEED ⚠️ java.util.Random seeded with the current timestamp. Insecure; handy for replaying time-window RNG flaws.
RANDOM_CUSTOM_SEED ⚠️ java.util.Random with user-supplied seed. Fully deterministic; useful for reproducible fuzzing or fixed keyspaces.
SHA1_PRNG ⚠️ Legacy “SHA1PRNG” engine (Android pre-2013). Lets you reproduce the historic SecureRandom bug.

Examples

🔐 SECURE_RANDOM

Best choice for real wallet generation – uses system CSPRNG (e.g. /dev/urandom, Windows CNG).

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "SECURE_RANDOM"
  }
],
// ...
🕰️ RANDOM_CURRENT_TIME_MILLIS_SEED

Recreates time-based vulnerabilities using java.util.Random seeded with the current system time.

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "RANDOM_CURRENT_TIME_MILLIS_SEED"
  }
],
// ...
🧪 RANDOM_CUSTOM_SEED

Fully deterministic PRNG using java.util.Random with a user-defined seed. Useful for reproducible scans and testing.

Fully deterministic output. Useful for reproducible tests or fixed keyspace scans. Without explicit seed:

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "RANDOM_CUSTOM_SEED"
  }
],
// ...

With custom deterministic seed:

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "RANDOM_CUSTOM_SEED",
    "customSeed": 123456789
  }
],
// ...
⚠️ SHA1_PRNG

Legacy deterministic PRNG using "SHA1PRNG". Used to reproduce the 2013 Android SecureRandom vulnerability. Can be used with or without an explicit seed.

Simulates old Android bug. May produce the same keys if seeded poorly or not at all. Without seed:

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "SHA1_PRNG"
  }
],
// ...

With custom seed:

// ...
"keyProducerJavaRandom": [
  {
    "keyProducerId": "exampleKeyProducerId",
    "randomAlgorithm": "SHA1_PRNG",
    "customSeed": 987654321
  }
],
// ...

🔐 BIP39_SEED (key producer java bip 39)

HD-wallet-style derivation: mnemonic + passphrase → BIP32/BIP44 keys.

Hierarchical deterministic key generator using a BIP39 mnemonic and optional passphrase. Allows full BIP32/BIP44 path derivation and reproducible HD wallets.

JSON field Type Default Purpose
mnemonic string 12/24-word BIP39 sentence
passphrase string "" Optional BIP39 salt (“wallet password”)
hardened boolean false Whether to use hardened key derivation (adds 0x80000000 to indices)
bip32Path string "M/44H/0H/0H/0" (constant DEFAULT_BIP32_PATH) Base derivation path; must start with M/
creationTimeSeconds number 0 Epoch-seconds creation timestamp; fed to DeterministicSeed.ofMnemonic

Minimal:

// ...
"keyProducerJavaBip39": [
    {
      "keyProducerId": "exampleKeyProducerId",
      "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
    }
],
// ...

Full:

// ...
"keyProducerJavaBip39": [
    {
      "keyProducerId": "exampleKeyProducerId",
      "mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
      "passphrase": "correct horse battery staple",
      "hardened" : false,
      "bip32Path": "M/44H/0H/0H/0",
      "creationTimeSeconds": 1650000000
    }
],
// ...

🧪 Runnable example: examples/config_Find_1CPUProducerBip39.json is a complete single-CPU-producer + single-consumer Find config that walks the BIP39 derivation sequence (batchUsePrivateKeyIncrement: false). Note: with batchUsePrivateKeyIncrement: true only the first derived key is used as an LSB-increment base (see the batch-mode limitation).

🔢 incremental scanning (key producer java incremental)

This mode generates private keys sequentially in batches within a specified key range. It is especially useful for:

  • Systematic scanning of a defined keyspace range (e.g., for key recovery or cryptographic research)
  • Batch processing optimized for GPU/OpenCL parallelization
  • Ensuring deterministic and reproducible key generation from start to end

Configuration Fields

JSON field Type Default Purpose
startPrivateKey string 0000000000000000000000000000000000000000000000000000000000000002 Hex string of the first private key in the range (inclusive)
endPrivateKey string FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 (secp256k1 group order) Hex string of the last private key in the range (inclusive)

How it works

  • Scanning begins at startPrivateKey, producing sequential private keys in batches of 2^batchSizeInBits keys each, up to and including endPrivateKey. See Batch Size per Kernel (batchSizeInBits) above for the exponent → batch-size mapping and typical values.
  • Each batch contains exactly 2^batchSizeInBits keys.
  • If the next full batch would go beyond endPrivateKey, the process stops with an exception.
  • The endPrivateKey is inclusive, but partial batches are not allowed by default — batches must fit completely inside the range.
  • For optimal performance, especially with OpenCL, configure batchSizeInBits (and the matching keysPerWorkItem) so that batches align perfectly within the range. This prevents errors and maximizes throughput.

Note: If unsure, set the endPrivateKey slightly higher to match a 2^batchSizeInBits multiple. This helps avoid exceptions and ensures efficient scanning.


Example JSON Configuration

Example minimal configuration:

// ...
{
  "keyProducerJavaIncremental": [
    {
      "keyProducerId": "exampleKeyProducerId",
      "startPrivateKey": "0000000000000000000000000000000000000000000000000000000000000002",
      "endPrivateKey": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
    }
  ]
}
// ...

🧩 This configuration incrementally searches a defined range of private keys. It is particularly suited for brute-force challenges, such as the 71st Bitcoin puzzle transaction.
The private key range is specified by two 64-character hex strings: startPrivateKey and endPrivateKey.

// ...
"keyProducerJavaIncremental": [
    {
      "keyProducerId": "exampleKeyProducerJavaIncremental",
      "startPrivateKey": "0000000000000000000000000000000000000000000000400000000000000000",
      "endPrivateKey":   "00000000000000000000000000000000000000000000007fffffffffffffffff"
    }
],
// ...

🌐 SOCKET_STREAM (key producer java socket)

Read raw private keys from a TCP socket stream (client or server mode).
Useful for piping externally generated secrets (e.g., from Python, Go, etc.) directly into the finder.

JSON Field Type Default Description
keyProducerId string Unique identifier for this key producer
mode string enum (CLIENT, SERVER) "SERVER" Whether to connect to a remote socket (CLIENT) or wait for connections (SERVER)
host string "localhost" Remote host to connect to (used only in CLIENT mode)
port number 12345 TCP port to connect to or bind on
timeoutMillis number 3000 Socket timeout in milliseconds, applied to accept (server), connect (client) and per-read. Must be non-negative
logReceivedSecret boolean false Whether to log each received private key as a hex string
connectRetryCount number 5 Number of times to retry establishing a socket connection (client mode)
connectRetryDelayMillis number 1000 Delay between connection retry attempts (in milliseconds)
maxWorkSize number 16777216 Maximum number of secrets to request in a single call

Minimal:

// ...
"keyProducerJavaSocket": [
    {
        "keyProducerId": "exampleKeyProducerId",
        "host": "localhost",
        "port": 12345,
        "mode": "SERVER"
    }
],
// ...

Full:

// ...
"keyProducerJavaSocket": [
    {
        "keyProducerId": "exampleKeyProducerId",
        "host": "localhost",
        "port": 12345,
        "mode": "SERVER",
        "timeoutMillis": 3000,
        "logReceivedSecret": true,
        "connectRetryCount": 5,
        "connectRetryDelayMillis": 1000,
        "maxWorkSize": 16777216
    }
],
// ...
Example: Python Socket Stream Server:
import socket
import os
import time
import binascii

HOST = 'localhost'
PORT = 12345

while True:
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((HOST, PORT))
            print(f"Connected to {HOST}:{PORT}")
            while True:
                private_key = os.urandom(32)
                print("Sending key:", binascii.hexlify(private_key).decode())
                s.sendall(private_key)
                time.sleep(1.0) # Slight delay to avoid flooding
    except ConnectionRefusedError:
        print("Connection refused, retrying in 1 second...")
        time.sleep(1)
    except BrokenPipeError:
        print("Connection lost, reconnecting...")
        time.sleep(1)
🔄 Protocol
  • Each private key must be exactly 32 raw bytes, sent binary, with no delimiter or framing.
  • Keys are interpreted as big-endian (new BigInteger(1, bytes) in Java).
  • Sending fewer than 32 bytes will cause blocking or exceptions.

🌐 SOCKET_STREAM (key producer via ZeroMQ)

Receive raw private keys via a ZeroMQ PULL socket. Ideal for decoupled key streaming where producers push keys using ZeroMQ PUSH sockets.

JSON field Type Default Description
keyProducerId string Unique identifier for this key producer
address string tcp://localhost:5555 The ZeroMQ address to bind or connect to
mode string enum (BIND, CONNECT) BIND Whether this socket binds to or connects
timeoutMillis number -1 Receive timeout in milliseconds:
0 = non-blocking,
-1 = infinite,
>0 = wait that long
logReceivedSecret boolean false Whether to log each received secret in hex

Minimal:

// ...
"keyProducerJavaZmq": [
    {
        "keyProducerId": "exampleKeyProducerId",
        "address": "tcp://localhost:5555"
    }
],
// ...

Full:

// ...
"keyProducerJavaZmq": [
    {
        "keyProducerId": "exampleKeyProducerId",
        "address": "tcp://localhost:5555",
        "mode": "BIND",
        "timeoutMillis": -1,
        "logReceivedSecret": true
    }
],
// ...
Example: Python Socket Stream Server:
import zmq
import os
import time
import binascii
from enum import Enum

class Mode(Enum):
    CONNECT = "connect"
    BIND = "bind"

ADDRESS = "tcp://localhost:5555"
MODE = Mode.CONNECT  # Change to Mode.BIND to switch

context = zmq.Context()

while True:
    try:
        socket = context.socket(zmq.PUSH)
        socket.setsockopt(zmq.SNDHWM, 1) # Limit send queue to 1 message

        if MODE == Mode.BIND:
            socket.bind(ADDRESS)
            print(f"Bound to {ADDRESS}")
        else:
            socket.connect(ADDRESS)
            print(f"Connected to {ADDRESS}")

        while True:
            private_key = os.urandom(32)
            print("Sending key:", binascii.hexlify(private_key).decode())
            try:
                socket.send(private_key, flags=zmq.NOBLOCK)
            except zmq.Again:
                print("Send queue full, message not sent.")
            time.sleep(1.0) # Slight delay to avoid flooding

    except zmq.ZMQError as e:
        print(f"ZMQ error occurred: {e}, retrying in 1 second...")
        time.sleep(1)

    finally:
        try:
            socket.close()
        except:
            pass
🔄 Protocol
  • Each private key must be exactly 32 raw bytes, sent as a single ZeroMQ message.
  • Messages are interpreted as unsigned big-endian (new BigInteger(1, bytes) in Java).
  • No framing or delimiters needed—ZeroMQ handles message boundaries.
  • Messages larger or smaller than 32 bytes will be discarded or raise errors.

📄 Secrets File Producer (producerJavaSecretsFiles)

Read pre-generated secrets from one or more text files instead of generating them at runtime. Unlike the entries above (which are key producers nested in a producer), this is a top-level producer placed directly under finder.producerJavaSecretsFiles. One secret per line; the line format is selected with secretFormat. See config_Find_SecretsFile.json.

JSON field Type Default Description
keyProducerId string Unique identifier for this producer
secretsFiles array of string [] Paths of the files to read (relative to the working dir)
secretFormat string enum STRING_SHA256 Line format of every file (see below)

Supported secretFormat values:

Value Meaning
BIG_INTEGER Decimal big integer per line (the raw private key as a base-10 number)
DUMPED_PRIVATE_KEY WiF (Wallet Import Format), e.g. 5K2YUVmWfxbmvsNxCsfvArXdGXm7d5DC9pn4yD75k2UaSYgkXTh
SHA256 Hex-encoded SHA-256 digest used directly as the private key
STRING_SHA256 Brainwallet: the line is hashed with SHA-256 and that digest is the private key
// ...
"producerJavaSecretsFiles": [
    {
        "keyProducerId": "exampleKeyProducerId",
        "secretsFiles": [ "secrets/fileContainingSecrets.txt" ],
        "secretFormat": "STRING_SHA256"
    }
],
// ...

Mixed Modes

You can combine vanity address generation with database lookups to enhance functionality and efficiency.

For example:

  • Search for personalized (vanity) addresses using custom patterns and
  • Simultaneously check if the generated addresses already exist in the LMDB database

This hybrid mode allows you to find rare or meaningful addresses while ensuring they haven’t been used before.

Key Range

You can define a custom key range for private key generation—for example, limiting the search space to 64-bit keys.
In this setup, the first 192 bits (256-bit - 64-bit) of the key are zeroed, effectively restricting the generation to a specific portion of the keyspace.

This feature can be used for:

  • Targeted key recovery, such as searching for private keys from known ranges like the Bitcoin Puzzle Transaction
  • Verification and testing, to prove that the software functions correctly within a predictable key range

OpenCL Acceleration

To significantly boost the performance of EC key generation, the software supports OpenCL-based parallelization.

A shared secret (base key) is transferred to the OpenCL device along with a predefined grid size. Each OpenCL thread independently derives a unique EC key by incrementing the base key with its thread ID. This allows the generation of a batch of EC keys in a single execution cycle. Once generated, the keys are transferred back to the host (CPU) memory for further processing.

The CPU then hashes the X and Y coordinates of the public keys to derive the corresponding (Bitcoin/Altcoin) addresses. This division of labor offloads the computationally expensive elliptic curve operations to the GPU, allowing the CPU to focus on faster address hashing and database lookup operations—resulting in improved overall throughput.

PoCL (Portable Computing Language) support

The OpenCL code is compatible with and tested against PoCL, an open-source, portable OpenCL implementation. PoCL lets you run the kernels without a vendor GPU driver and exposes a range of specialized targets (backends) that PoCL supports — for example CPU (host), CUDA, and other device backends — so you can pick the device that best fits your hardware. Select the desired PoCL device through the usual producerOpenCL platformIndex / deviceIndex settings (use the OpenCLInfo command to list the platforms and devices PoCL exposes).

Built-in Self-Test (BIST)

The OpenCL backend includes a built-in self-test mechanism that cross-verifies results from the GPU against a CPU-generated reference. This ensures that the OpenCL device is functioning correctly and producing valid EC keys—giving end users confidence in the reliability of their hardware-accelerated address search.

💡 Hint: To enable the built-in self-test, make sure the following configuration flag is set to true:

"runtimePublicKeyCalculationCheck": true

This option is disabled by default. Enabling it is especially useful during development, debugging, or when validating new hardware setups.

⚠️ BIP-39 Deterministic Keys and Batch Mode Limitation

If you configure a BIP-39 key producer (e.g. keyProducerJavaBip39) together with OpenCL acceleration, please read this carefully.

The BIP-39 key producer creates deterministic private keys derived from your mnemonic, passphrase, and BIP32 path.
This derivation space is finite — once all child keys along the defined path are exhausted, the producer will throw
a NoMoreSecretsAvailableException. This can happen relatively quickly in GPU batch configurations.

In your JSON configuration, you may have enabled:

/**
 * The batch mode will use a private key increment internal to increase the performance.
 */
public boolean batchUsePrivateKeyIncrement = true;

When batchUsePrivateKeyIncrement is set to true, the OpenCL producer calls:

secrets = keyProducer.createSecrets(cProducer.getOverallWorkSize(bitHelper), cProducer.batchUsePrivateKeyIncrement);

Internally this sets returnStartSecretOnly = true, meaning that only the first BIP-39-derived key is passed to the GPU.
The OpenCL kernel then uses that single key as a base key, incrementing the least significant bits (LSB) in parallel across all GPU threads to explore adjacent keyspaces.

⚠️ Use extreme caution:

  • The effective search space on the GPU is not the same as the full BIP-39 derivation sequence.
  • This mode is intended purely for experimental or high-performance batch testing — not for full HD-wallet traversal.
  • Once all deterministic child keys are consumed, a NoMoreSecretsAvailableException will occur.

For technical details, see:

Performance Benchmarks

Note: the kernel computes each public-key point once (the uncompressed form); the compressed form is derived from the same point with negligible overhead, and both the compressed and uncompressed address are checked. Effective Keys/s counts keys (EC points), so the number of addresses examined is twice this rate — see Keys versus addresses.

GPU Model CPU Key Range (Bits) Grid Size (Bits) keysPerWorkItem Effective Keys/s (~)
NVIDIA RTX 3070 Laptop AMD Ryzen 7 5800H 256 22 256 ~229,000,000 keys/s
NVIDIA RTX 3070 Laptop AMD Ryzen 7 5800H 256 24 2048 ~266,000,000 keys/s
AMD Radeon RX 7900 XTX AMD Ryzen 7 9800X3D 256 24 512 ~670,000,000 keys/s

Methodology. Compact GPU-filter fast path, candidates/s = JMH ops/s × 2^batchSizeInBits, all on the inlined kernel (the example configs now default to noInlineHelpers: false). Each device is shown at its own tuned optimum — the grid optimum is device-specific: the RTX 3070 peaks at kpwi=2048, the RX 7900 XTX at kpwi=512 (kpwi=2048 collapses to ~71 M/s on RDNA3 through under-occupancy). The 3070's batch=22 row is kept as a lower-grid reference. Live end-to-end Find runs (tuned FUSE_16 + BINARY_FUSE_8 cascade against the ~141 M-entry light DB, warm) sustained ~280 M/s on the 3070 and ~616 M/s on the 7900 XTX. Raw data: tune_arms.csv (RTX 3070) and tuner_ryzen9800x3d_gfx1100.csv (RX 7900 XTX).

The inlined kernel is the dominant lever on AMD. At the same 24/512 arm the 7900 XTX runs 187.8 M/s out-of-lined vs 669.8 M/s inlined — a ~3.6× gap that had hidden the card's real speed (an out-of-lined sweep capped at batch=22 reported only ~130 M/s). With the inlined kernel the 7900 XTX now sits ~2.3–2.5× above the 3070, as its raw compute would suggest. Trade-off: on AMD the inlined kernel's first compile is slow (~8–16 min, then comgr-cached); NVIDIA compiles it instantly.

Grid Size (Bits) (= batchSizeInBits) and keysPerWorkItem are device-specific — sweep your own hardware with GridSizeSweepBenchmark / TuneConfiguration. Key Range = 256 is the full-width private-key path; a 160-bit MSB-Zero scan runs faster but is not separately benchmarked here. Earlier third-party rows (RTX 3090, A3000, Radeon 8060S) and their pre-optimization figures were removed as obsolete.

GPU Binary Fuse 8 filter — compact output vs. full transfer

The table above shows the tuned peak throughput on the compact GPU-filter fast path. The table below isolates the effect of the GPU-side Binary Fuse 8 filter (enableGpuFilter) at a fixed grid: with it on, the kernel checks each derived hash160 against the on-GPU filter and transfers only the hits; with it off, every work-item result is read back over PCIe.

Note: These numbers come from the GpuFuse8FilterBenchmark JMH microbenchmark (one kernel launch + PCIe read-back + host-side parse, measured in isolation). They are not directly comparable to the full-pipeline figures above — the grid size differs and there is no real consumer/queue — but the on-vs-off ratio on the same row is the metric that matters.

GPU Filter (enableGpuFilter) GPU Model Key Range (Bits) Grid Size (Bits) Effective Keys/s (~) Speedup
Off — full transfer (legacy) NVIDIA RTX 3070 Laptop 256 19 4,350,000 keys/s 1.00× baseline
On — Binary Fuse 8 compact NVIDIA RTX 3070 Laptop 256 19 9,710,000 keys/s ~2.2×

Measured against a ~1 M-entry filter at batchSizeInBits = 19 (2¹⁹ ≈ 0.52 M candidates per launch), matching the grid size of the raw key-generation table above. The GpuFuse8FilterBenchmark JMH benchmark A/B-toggles compact mode against the legacy full-transfer path on the same kernel and batch; numbers above are the mean of 3 measurement iterations (compact 18.52 ± 0.05 ops/s, very stable; full transfer 8.30 ops/s, noisier) — each op is one launch of 2¹⁹ candidates. The relevant one-time cost is OpenCL kernel compilation (done once in setup) and GPU clock ramp-up, not JVM warmup. The filter wins by collapsing the read-back from the full grid down to the ~0.4 % false-positive hits, so the advantage grows with batchSizeInBits. Run it yourself:

mvn test-compile exec:java -Dexec.args="GpuFuse8FilterBenchmark"

Device-side breakdown (where the time actually goes). The throughput above mixes three costs: GPU kernel compute, the PCIe read-back, and the host-side parse of the returned grid. To separate them, set enableProfiling: true on the OpenCL producer (or run the benchmark with -p profiling=true); the command queue is then created with CL_QUEUE_PROFILING_ENABLE and each launch is timestamped on the device. The same grid-19 run on the RTX 3070 reports:

Phase Off — full transfer On — Binary Fuse 8 compact Effect
GPU kernel compute 51.5 ms 54.8 ms +3.3 ms (~6.5%) — the inline filter check
PCIe read-back (device) 8.75 ms 0.059 ms ~150× less — only the ~0.4% hits cross the bus
Host-side parse (wall − device) ~58.6 ms ~0.3 ms ~185× less — the consumer parses ~2 k hits, not 524 k results
Per-launch wall time ~118.8 ms ~55.2 ms

The takeaway: the GPU-side filtering is not expensive on the GPU. It adds only ~6.5 % to kernel compute (a handful of integer ops + one murmur64 hash + 3 fingerprint reads per candidate, versus the elliptic-curve math and two SHA-256 + two RIPEMD-160 hashes that dominate the kernel). The ~2.2× end-to-end gain comes almost entirely from collapsing the PCIe transfer (~150×) and the host-side parse (~185×) — i.e. it offloads the address-presence check from the CPU at negligible GPU cost, which is exactly the design intent. (Profiling adds a little driver overhead and is off by default; it is a diagnostic switch, never enabled by the runtime pipeline.)

Logging and Runtime Statistics

In Find mode the consumer logs a periodic, single-line statistics snapshot at INFO level. The print interval is controlled by consumerJava.printStatisticsEveryNSeconds (default 60); the Generated and -> LMDB rates are a current throughput averaged over a trailing consumerJava.statisticsRateWindowSeconds window (default 60), independent of the print interval. All logging goes through SLF4J / Logback, so format and destinations are fully configurable (see Customizing Log Output).

Reading the Statistics Line

A statistics line looks like this:

Statistics: [uptime 43 min] [Generated 128 M/s (333258 M total)] [-> LMDB 2 M/s (5196 M lookups total), 99.22% pre-filtered] [rate window 60s] [Batches per producer: exampleKeyProducerSecureRandomId (Random, GPU)=79455] [Producers running: 1] [Consumers running: 8] [Consumer ready for work (queue empty): 167939] [Producer blocked (queue full): 0] [Average contains time: 0 ms] [keys queue size: 0] [Hits: 0]
Field Meaning
uptime N min Lifetime elapsed minutes since the scan started.
Generated <rate> (<N> M total) The headline performance number: the rate of candidate private keys the producers fully compute (windowed rate + lifetime total in millions). On a GPU run this is the real key-generation output. It counts keys (EC points), not addresses — see Keys vs. addresses below. Auto-scales across /s, k/s, M/s, G/s.
-> LMDB <rate> (<N> M lookups total), NN.NN% pre-filtered The rate of hash160 address lookups that survive the GPU pre-filter and actually reach the consumer/LMDB (windowed rate + lifetime total). pre-filtered is the share of the potential address lookups the filter eliminated before LMDB (see below); it is omitted when no pre-filter is active (full transfer).
rate window Ns The trailing window (statisticsRateWindowSeconds) the two rates are averaged over.
Batches per producer: <label>=N, … Dispatched-batch count per running producer, keyed by <keyProducerId> (<Strategy>, <CPU|GPU>) so concurrently running producers are told apart. The strategy (Random, Bip39, Incremental, Socket, WebSocket, Zmq) is derived from the key producer; the backend (CPU for producerJava/producerJavaSecretsFiles, GPU for producerOpenCL) from the producer. none until the first batch.
Producers running: N Number of producers currently in the RUNNING state.
Consumers running: N Number of consumer worker threads currently running (≤ consumerJava.threads).
Consumer ready for work (queue empty): N Runtime health counter — rising is normal/healthy. Consume cycles that found the queue empty and waited. An empty queue is the desired state: it means the CPU drains everything the producers generate and has headroom. See below.
Producer blocked (queue full): N Runtime health counter — rising is the warning sign. Times a producer hit a full queue and had to wait, i.e. the CPU can't keep up (CPU-bound). See below.
Average contains time: N ms Mean time spent per address-presence lookup. Large values point at a slow lookup backend (see Address Lookup Backends).
keys queue size: N Instantaneous depth of the producer→consumer queue (bounded by consumerJava.queueSize).
Hits: N Number of address matches found so far (see Hit Logging).

Keys versus addresses

Why Generated and -> LMDB count different things: Generated counts candidate private keys — secp256k1 points, one full scalar multiplication each. From every such key the tool derives two hash160 addresses: the compressed and the uncompressed form, and both are checked. So the number of addresses examined is twice the Generated rate:

  • Generated 128 M/s ⇒ 128 M keys/s ⇒ ~256 M addresses/s (compressed + uncompressed) hashed and probed against the filter.
  • -> LMDB counts those address lookups (post-filter), not keys. With a strong GPU pre-filter almost none survive, so -> LMDB is a tiny fraction of Generated.
  • pre-filtered is measured against that doubled figure: it is the share of the 2 × Generated potential address lookups the pre-filter removed before LMDB. In the line above, 2 M/s reach LMDB out of ~256 M/s potential ⇒ 99.22 % pre-filtered.

This split is deliberate. An earlier version reported only the surviving (-> LMDB) rate, which collapses when a GPU pre-filter is on — making a hard-working GPU look idle. Generated now shows the real output rate next to it, so a healthy GPU run reads as high Generated, low -> LMDB, high pre-filtered.

The total fields (Generated … total, -> LMDB … total, Consumer ready, Producer blocked, Hits) are cumulative since startup — for those, what matters is how fast they rise between two lines, not the absolute value. The Generated / -> LMDB rates are already a current windowed rate, so they read directly with no need to difference successive lines.

Diagnosing Bottlenecks

The pipeline is Producers → bounded queue → Consumer (CPU address checks). The queue is the pressure gauge, and the two counters have opposite polarity — only one of them is a problem signal:

  • Producer blocked (queue full) — this is the warning sign. It rises when producers generate batches faster than the consumer can check them, so the queue fills and producers must wait in put(). A sustained rise means the consumer/CPU is the bottleneck (CPU-bound). This is the counter to watch if throughput is lower than expected.
  • Consumer ready for work (queue empty) — rising is normal and expected, not a problem. It rises when the consumer drains the queue and waits for more work. You want the queue as empty as possible: it means the CPU comfortably keeps up with the producers and has spare capacity. For GPU scanning this counter rising steadily (with keys queue size near 0) is exactly the healthy operating point. It is only worth a second look if you expected the producers to saturate the CPU and they don't, or if nothing is producing (a stalled or misconfigured producer also shows as a perpetually ready consumer with zero throughput).
Producer blocked Consumer ready keys queue size Interpretation
~0 rising ~0 Healthy (the normal GPU-scanning state) — the CPU keeps up easily. If throughput is below expectations, the limit is the producer/GPU side, not the consumer.
rising ~0 near queueSize CPU-bound — the warning state. The consumer can't keep up. Use a faster lookup backend, raise consumerJava.threads, or reduce the producer rate.
~0 ~0 mid-range Balanced — neither side waits; watch Generated.
~0 rising ~0 with Generated ≈ 0 → nothing is producing — check the producer/key-producer configuration.

Rule of thumb: Producer blocked climbing = act (CPU too slow); Consumer ready climbing = fine.

Hit Logging

When a generated address matches the database, the consumer logs it at INFO with a clear prefix and the full key material needed to recover the wallet:

  • hit: Found the address: … — the match, including the private key in WIF, the public address, and related key details.
  • hit: safe log: … — an additional, separately formatted record written immediately on a hit so the finding survives even if later formatting fails.
  • vanity pattern match: … — emitted when optional vanity-pattern matching (consumerJava.enableVanity / vanityPattern) is enabled and a generated address matches.

The running Hits: field in the statistics line reflects the total count. To persist hits to their own file, route these messages with a dedicated appender (next section).

Customizing Log Output

Logging is backed by Logback. A ready-to-edit configuration ships at examples/logbackConfiguration.xml. Point the application at a custom file with:

java -Dlogback.configurationFile=/path/to/logbackConfiguration.xml -jar bitcoinaddressfinder-<version>-jar-with-dependencies.jar config.json

Typical customizations:

  • Write hits to their own file — add a FileAppender and route the ConsumerJava logger (or filter on the hit: prefix) to it, so matches are never lost in console scrollback.
  • Adjust the statistics cadence / rate windowconsumerJava.printStatisticsEveryNSeconds controls when the line is emitted; consumerJava.statisticsRateWindowSeconds controls the trailing window the Generated / -> LMDB rates are averaged over (Logback controls how/where).
  • Machine-readable output — the single-line, fixed-field statistics format is intentionally stable so an external supervisor or GUI can parse it (e.g. via an inter-process pipe) to plot throughput and the ready/blocked health counters over time.

Keep the CRLF guard. The bundled pattern wraps the message in %replace(%msg){'[\r\n]+', ' | '}, which neutralizes carriage returns / line feeds in rendered messages so untrusted input cannot forge extra log lines. Preserve this wrap in any derived pattern.

Collision Probability and Security Considerations

Isn't it impossible to find collisions?

The likelihood of discovering a collision—two different inputs that produce the same output hash—is astronomically low when using secure cryptographic functions like SHA-256 and RIPEMD-160, which are employed in Bitcoin address generation.

However, discussions around potential vulnerabilities, theoretical attacks, or edge cases exist in the cryptography and Bitcoin communities.

For more in-depth information on collision resistance, address reuse risks, and cryptographic hash functions, refer to the following resources:

⚠️ Security Advisory: Android RNG Vulnerability (2013) and Simulation via BitcoinAddressFinder

In 2013, a serious vulnerability was discovered in Android’s SecureRandom implementation. It caused Bitcoin private keys to be exposed due to reused or predictable random values during ECDSA signature creation. This problem affected many wallet apps that generated keys directly on Android devices.

BitcoinAddressFinder can be used to simulate and analyze this type of attack. With small changes, it can reproduce faulty random number generators by:

  • using fixed or repeating k values
  • limiting entropy to 16, 32, or 64 bits
  • replacing the secure RNG with a weak or deterministic version

This makes it possible to test and study:

  • how r-collisions happen in ECDSA signatures
  • how easy it is to find reused k values
  • how quickly a private key can be recovered
  • how secure different RNG implementations really are

BitcoinAddressFinder can generate millions of key pairs quickly. This allows researchers to create and scan large keyspaces under controlled RNG conditions. All results can be verified using the built-in self-test feature or compared against known addresses in the LMDB database.

This kind of simulation is useful for:

  • learning about signature security
  • building training examples for audits or courses
  • checking RNG quality in real wallets or custom apps

References:

Example: Weak RNG in an embedded Ethereum wallet (TinyMT32 seeded from micros())

Issue: AlphaWallet/Web3E#31

Credits / Found by Jean-Philippe Aumasson: https://github.com/veorq

KeyID::generatePrivateKey() relies on random_buffer(), which uses tinyMT32 as its PRNG. The PRNG is initialized only once using a 32-bit seed derived from micros().

This is not a cryptographically secure RNG, and the 32-bit timer seed makes the generated private keys predictable and brute-forceable. All keys produced by the current implementation are vulnerable. A hardware CSPRNG or proper DRBG must be used instead.

Links:

Although this vulnerability originates from an Ethereum-based embedded wallet, it demonstrates the same class of weaknesses relevant to Bitcoin and all ECDSA-based systems: private keys generated from low-entropy or predictable random sources.

In this implementation, the wallet uses TinyMT32 as its PRNG and seeds it only once with a 32-bit microsecond timer:

tinymt32_init(&tinymt, (uint32_t)micros());

TinyMT32 is not a cryptographically secure RNG, and seeding it with a 32-bit timestamp collapses the effective key space from 2²⁵⁶ to only 2³² possible seeds. An attacker who can approximate the device’s boot time can brute-force all feasible seeds, reproduce the PRNG stream, and reconstruct the wallet’s private keys.

This resembles the historic Android SecureRandom vulnerability: the elliptic-curve cryptography itself is secure, but the randomness used to create keys is not.

BitcoinAddressFinder can simulate this type of scenario by generating keys using intentionally weak or deterministic RNGs and scanning the resulting restricted key ranges. This makes it possible to study how insecure RNGs can compromise wallets.

Similar projects

How BitcoinAddressFinder (pinned first, in bold) compares to related tools. Its distinguishing mix — GPU and CPU generation, 100+ coins, and an offline check against a local database of known/funded addresses — is rare here: most others do just one of vanity generation, puzzle-solving, online balance lookups, or wallet recovery. It also stays efficient against a ~10⁹-entry address database (≈1.38 billion / ~57 GB) via an O(1) memory-mapped LMDB lookup with an optional Binary Fuse / Blocked Bloom pre-filter — the axis (≥10⁹ DB) most other tools do not scale on.

Legend: ✅ yes · ❌ no · ❓ could not confirm · ~ partial. Columns: Category = what the tool is for · GPU / CPU = compute backend the user runs · Coins = many non-Bitcoin coins · Vanity = pattern/prefix matching · Offline = the search runs without contacting a server/API · OSS = source publicly available · ≥10⁹ DB = efficient (O(1) / indexed) lookup against a very large (~10⁹) known-address set ( yes · ~ indexed but at smaller scale · linear / tiny / online / none) · Checks against = what a derived address is matched against — Local funded DB · Target list (usually user-supplied) · Online API · Puzzle target · Own wallet · None (generate-only).

Flags verified 2026-07-20 against each project's repository or site; the few cells that stayed unconfirmable are marked ❓ — corrections via PR are welcome. Rows are grouped by Category (BitcoinAddressFinder pinned first). Categorisation is for orientation, not an endorsement.

Project Category GPU CPU Coins Vanity Offline OSS ≥10⁹ DB Checks against Main goal
BitcoinAddressFinder Address-DB scan Local funded DB Scan random/sequential keys for 100+ coins vs a local LMDB address database; optional vanity
LBC Address-DB scan ~ Local funded DB Distributed pool scanning the hash160 space for funded-address collisions (legacy)
privatekeys.pw — scanner Address-DB scan ~ Local funded DB In-browser random-key scanner against a hosted funded-address filter
allprivatekeys.com — get lucky Address-DB scan Online API Browse server-generated key pages (BTC/BCH/BTG) with tx/balance flags
treyyoder/bitcoin-wallet-finder Address-DB scan Online API Random keys, balances checked via the blockchain.info API (demo)
Frankenmint/PKGenerator_Checker Address-DB scan Online API Random keys, balances checked via an online explorer API (demo)
Henshall/BitcoinPrivateKeyHunter Address-DB scan Target list Random keys vs a user-supplied local list of known-funded addresses
Xefrok/BitBruteForce-Wallet Address-DB scan Local funded DB Random keys vs a bundled local ~123k-address list (linear scan)
Isaacdelly/Plutus Address-DB scan ~ Local funded DB Sequential keyspace walk vs a local funded-wallet DB (Bloom filter)
Noname400/Hunt-to-Mnemonic Address-DB scan ~ Local funded DB BIP39 / seed hunt vs local Bloom-filter address databases
Py-Project/Bitcoin-wallet-cracker Address-DB scan Target list Random BIP39 wallets vs a local address list (offline GUI)
vlnahp/KeyZero Address-DB scan Local funded DB Random/sequential keys vs a local address list (or online API)
albertobsd/keyhunt Puzzle solver Target list CPU hunter with several modes (address, BSGS, xpoint) over a bit range
brichard19/BitCrack Puzzle solver Target list GPU brute-force of a keyspace range for target addresses
kanhavishva/KeyHunt-Cuda Puzzle solver Target list CUDA hunter for puzzle/target addresses over a range (VanitySearch-based)
privatekeys.pw — cloud search Puzzle solver Puzzle target Rent cloud GPUs to scan for the Bitcoin puzzle (pool)
ilkerccom/bitcrackrandomiser Puzzle solver Puzzle target Pool client that drives BitCrack for the Bitcoin puzzle
btcpuzzle.info Puzzle solver Puzzle target Solo-pool platform + open-source client for the Bitcoin puzzle
JeanLucPons/VanitySearch Vanity generator None GPU/CPU vanity address generator (prefix + wildcards)
samr7/vanitygen Vanity generator None The original vanity generator (CPU + OpenCL, regex)
10gic/vanitygen-plusplus Vanity generator None Multi-coin vanity generator (100+ coins; CPU / OpenCL)
oritwoen/vgen Vanity generator None Regex-driven vanity generator (Rust; wgpu or CPU)
allprivatekeys.com — vanity Vanity generator None In-browser vanity generator; keys stay local (can run offline)
gurnec/btcrecover Wallet recovery Own wallet Recover your own wallet password / BIP39 seed (optional AddressDb)
Coding-Enthusiast/FinderOuter Wallet recovery Own wallet Reconstruct your own damaged key / seed / address (GUI)
johncantrell97/bip39-solver-gpu Wallet recovery Own wallet GPU brute-force of missing BIP39 words for a known address
JeanLucPons/BTCCollider Research / collision None Find RIPEMD-160 address collisions (birthday paradox)
mvrc42/bitp0wn Research / collision None Educational ECDSA nonce-reuse / discrete-log attack demos
JamieAcharya/Bitcoin-Private-Key-Finder Novelty / utility Target list Random keys vs one user-supplied target address (novelty)
mingfunwong/all-bitcoin-private-key Novelty / utility None Web page to browse the whole key space (no balance check)
prof7bit/wallet-key-tool Novelty / utility None GUI to import/export/convert wallet keys (no search)

Deep learning private key prediction

An export of the full database can be used to predict private keys with deep learning. A funny idea: https://github.com/DRSZL/BitcoinTensorFlowPrivateKeyPrediction

Learn more

Known Issues

Hybrid Graphics Performance (Low Throughput)

Laptops with hybrid graphics—using both integrated (iGPU) and discrete (dGPU) GPUs—may suffer from significantly reduced performance when running compute-intensive OpenCL workloads like BitcoinAddressFinder. This is often due to:

  • Shared memory between CPU and GPU
  • Bandwidth limitations
  • Automatic GPU switching (e.g., NVIDIA Optimus, AMD Enduro)
  • Suboptimal GPU selection by drivers

Affected Devices and Recommendations

Manufacturer Affected Series/Models Recommendation
HP ZBook G3, G4, G5, G7 Enter BIOS → set Graphics to Discrete Only
Lenovo ThinkPad X, T, and P Series (hybrid configs) Use Lenovo Vantage → Disable Hybrid Graphics
Dell Inspiron, XPS, Precision (with Optimus) BIOS → Disable Hybrid Mode (if available)
MSI Gaming laptops with switchable graphics Use Dragon Center to select dGPU
Razer Blade models with NVIDIA Optimus Use Razer Synapse to enforce dGPU use
Apple (Intel) MacBook Pro (pre-2021 with dual GPUs) macOS → Disable Automatic Graphics Switching in Energy Preferences

If your laptop uses hybrid graphics, always ensure that the discrete GPU is explicitly selected for OpenCL workloads to avoid severe performance bottlenecks.

Contributors: do not upgrade jqwik past 1.9.3

⚠️ DO NOT UPGRADE jqwik past 1.9.3. jqwik 1.10.0 added an anti-AI prompt-injection string to test stdout; the 1.10.1 user guide states the library "is not meant to be used by any 'AI' coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version. See CLAUDE.md section "jqwik prompt-injection in test output" for the full context. Dependabot is configured to ignore all net.jqwik updates (every version, including patches) — see the ignore rule in .github/dependabot.yml.

Future improvements

  • Expand PIT mutation-testing scope. PIT is wired in pom.xml and runs on every CI build (in the ubuntu-latest leg of the test job) with <mutationThreshold>100</mutationThreshold>, but <targetClasses> is currently narrowed to a single class (BitHelper). The intent is to exercise the wiring and gate against regressions on that single class today; widen <targetClasses> incrementally as additional classes reach mutation-test parity. Final target: <param>net.ladenthin.bitcoinaddressfinder.*</param> matching the streambuffer pattern (excluding native/OpenCL-bound code where mutation testing is impractical).
  • Refactor the entire key generation infrastructure to support a key provider. This provider should be configurable to supply private keys from various sources, such as Random, Secrets File, Key Range, and others. All consumers should retrieve keys from this provider.

KeyProvider

  • Key generation within a specific key range. See #27 Wished from themaster:
"privateKeyStartHex" : "0000000000000000000000000000000000000000000000037e26d5b1f3afe216"
"privateKeyEndHex" : "0000000000000000000000000000000000000000000000037e26d5b1ffffffff"

Wished from Ulugbek:

// Search started from given address. Would be nice if it can save last position...
"sequentalSearch" : true,
"startPrivateKey" : xxxxxxxx,

// Random search with batches, here 100000. I,e. some random number is found and after 100000 sequental addresses should be checked.
"searchAsBatches" : true,
"searchBatchQuantity" : 100000,


// Random search within Address Space, with batches, here 100000.
"searchAsBatches" : true,
"searchAddressStart" : xxxxxxx,
"searchAddressEnd" : xxxxxxxy,
"searchBatchQuantity" : 100000
  • Incomplete Seed-Phrase as Private KeyProvider. Wished from @mirasu See #38
  • Socket KeyProvider for independend KeyProvider via byte protocol
    • Ideas might be a screen recorder and use the visible screen downscaled as 256 bit input
  • KeyProvider must get the grid size to increment properly on incremental based Producer
  • ExecutableKeyProvider gets data from stdout

Acknowledgements

BitcoinAddressFinder stands on the shoulders of excellent open-source work. In particular:

  • hashcat (hashcat.net) — the OpenCL secp256k1 elliptic-curve, SHA-256 and RIPEMD-160 routines that power GPU key generation are ported from hashcat's MIT-licensed kernels in OpenCL/ (vendored here under src/main/resources/copyfromhashcat/). Huge thanks to the hashcat project and its contributors.
  • bitcoinj — Bitcoin/altcoin key derivation and address encoding used across the CPU path and as the reference for the GPU parity tests.
  • LMDB / lmdbjava — the high-performance memory-mapped address database.
  • JOCL — Java bindings to OpenCL.
  • Binary Fuse filters — by Thomas Mueller Graf and Daniel Lemire, with the SipHash construction by Jean-Philippe Aumasson and Daniel J. Bernstein.
  • vanitygen (Samr7) and vanitygen-plusplus (10gic) — pioneering CPU/OpenCL Bitcoin vanity-address searchers; studied for their GPU EC key-generation techniques.
  • bitcoin-core/secp256k1 — the reference high-performance secp256k1 library (reduced-radix field, GLV endomorphism, safegcd modular inverse); the basis for several of the EC optimizations and a reference for correctness.
  • bitcoinj/secp256k1-jdk — secp256k1 bindings for the JVM; a reference for the Java-side secp256k1 surface.

See the bundled third-party licenses for full attribution; some subprojects are under their own licenses (e.g. the copyfromhashcat kernels are MIT).

Legal

BitcoinAddressFinder is not intended for malicious use, and you are solely responsible for complying with your local laws, international regulations, and ethical standards.

This software must not be configured or used to attempt unauthorized access to cryptocurrency assets (e.g., scanning for RIPEMD-160 address collisions to gain access to third-party funds).
Such activities are likely illegal in most jurisdictions and may carry serious penalties.


✅ Permitted Use Cases

You may use BitcoinAddressFinder for legitimate and research-focused purposes, such as:

  • Recovering lost private keys associated with your own known public addresses
  • Verifying whether generated addresses have ever been used to prevent collisions
  • Running performance benchmarks and OpenCL testing
  • Generating vanity addresses for personal or demonstrative use
  • Conducting offline cryptographic research and educational exploration

🚫 Prohibited Use Cases

You must not use this tool for:

  • Gaining unauthorized access to cryptocurrency or wallet funds
  • Circumventing access controls or exploiting systems
  • Engaging in unethical behavior or violating terms of service

Legal References by Jurisdiction

Below is a non-exhaustive collection of legal frameworks that may apply depending on your jurisdiction.

⚠️ Important Note: The following information is provided for informational purposes only and does not constitute legal advice.
You should always consult with a qualified legal professional before engaging in any activity related to cryptographic systems or asset recovery.
I merely present relevant legal context, not binding interpretations or actionable recommendations.

Germany

  • § 202c StGBVorbereiten des Ausspähens und Abfangens von Daten
    (Preparation of spying or intercepting data)

  • OLG Braunschweig, Beschluss vom 18.09.2024 – 1 Ws 185/24
    In einem aufsehenerregenden Fall entschied das OLG, dass der Zugriff auf eine Wallet mittels eines bekannten (nicht rechtswidrig erlangten) Seeds nicht als Straftat im Sinne der §§ 202a, 263a oder 303a StGB gewertet werden kann.
    Der Angeklagte hatte eine Wallet für einen Dritten erstellt und sich später mittels der Seed-Phrase Zugriff auf Token im Wert von rund 2,5 Mio. € verschafft.
    Das Gericht urteilte jedoch:

    • Kein Diebstahl: Kryptowährungen sind keine „Sachen“ im Sinne des § 242 StGB.
    • Kein Ausspähen von Daten (§ 202a StGB): Der Zugriff mittels bekannter Passwörter ist kein Überwinden einer Zugangssicherung.
    • Kein Computerbetrug (§ 263a StGB): Eine Krypto-Transaktion impliziert keine „Täuschung“ oder „Miterklärung einer Berechtigung“.
    • Keine Datenveränderung (§ 303a StGB): Die eigentliche Änderung erfolgt durch die Blockchain-Netzwerkbetreiber – nicht durch den User selbst.

    🔍 Fazit: Der „Kryptodiebstahl“ per bekanntem Seed ist unter Umständen nicht strafbar – bleibt aber zivilrechtlich angreifbar.
    Quelle: OLG Braunschweig Beschluss 1 Ws 185/24 (juris.de)
    Bericht: heise.de Artikel vom 11.07.2025

United States

  • Computer Fraud and Abuse Act (CFAA)
    Prohibits unauthorized access to protected computers, including use of tools for circumvention

  • Digital Millennium Copyright Act (DMCA)
    Sections on anti-circumvention tools and reverse engineering may apply in specific contexts

European Union

  • General Data Protection Regulation (GDPR)
    While not directly related, use of personally identifiable data or address targeting must comply

  • Directive 2013/40/EU on Attacks Against Information Systems
    Criminalizes the creation or possession of tools designed for unauthorized access

Other Notable References

  • UK: Computer Misuse Act 1990
  • Canada: Criminal Code Sections 342.1 & 430
  • Australia: Criminal Code Act 1995 – Part 10.7 – Computer Offences

⚠️ The authors and maintainers of BitcoinAddressFinder assume no responsibility for how the tool is used.
It is your duty to ensure compliance with all relevant legal and ethical standards in your country and jurisdiction.

If in doubt, consult with a legal professional before using this software for anything beyond educational or personal purposes.

License

It is licensed under the Apache License, Version 2.0. See LICENSE for the full license text. Some subprojects have a different license.

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

FOSSA Status