• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bernardladenthin / BitcoinAddressFinder / #306

20 Apr 2025 09:43AM UTC coverage: 68.475% (+0.03%) from 68.442%
#306

push

bernardladenthin
Refactor public key validation and hash checking into reusable method; improve test coverage.

33 of 33 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

1199 of 1751 relevant lines covered (68.48%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.75
/src/main/java/net/ladenthin/bitcoinaddressfinder/ConsumerJava.java
1
// @formatter:off
2
/**
3
 * Copyright 2020 Bernard Ladenthin bernard.ladenthin@gmail.com
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *    http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
// @formatter:on
19
package net.ladenthin.bitcoinaddressfinder;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import java.nio.ByteBuffer;
23
import java.time.Duration;
24
import java.time.temporal.ChronoUnit;
25
import java.util.ArrayList;
26
import java.util.List;
27
import java.util.concurrent.ExecutorService;
28
import java.util.concurrent.Executors;
29
import java.util.concurrent.Future;
30
import java.util.concurrent.LinkedBlockingQueue;
31
import java.util.concurrent.ScheduledExecutorService;
32
import java.util.concurrent.TimeUnit;
33
import java.util.concurrent.atomic.AtomicBoolean;
34
import java.util.concurrent.atomic.AtomicLong;
35
import java.util.regex.Matcher;
36
import java.util.regex.Pattern;
37
import net.ladenthin.bitcoinaddressfinder.configuration.CConsumerJava;
38
import net.ladenthin.bitcoinaddressfinder.persistence.Persistence;
39
import net.ladenthin.bitcoinaddressfinder.persistence.PersistenceUtils;
40
import net.ladenthin.bitcoinaddressfinder.persistence.lmdb.LMDBPersistence;
41
import org.apache.commons.codec.binary.Hex;
42
import org.bitcoinj.crypto.ECKey;
43
import org.bitcoinj.crypto.MnemonicException;
44
import org.slf4j.Logger;
45
import org.slf4j.LoggerFactory;
46

47
public class ConsumerJava implements Consumer {
48

49
    /**
50
     * We assume a queue might be empty after this amount of time.
51
     * If not, some keys in the queue are not checked before shutdow.
52
     */
53
    @VisibleForTesting
54
    static Duration AWAIT_DURATION_QUEUE_EMPTY = Duration.ofMinutes(1);
1✔
55
    
56
    public static final String MISS_PREFIX = "miss: Could not find the address: ";
57
    public static final String HIT_PREFIX = "hit: Found the address: ";
58
    public static final String VANITY_HIT_PREFIX = "vanity pattern match: ";
59
    public static final String HIT_SAFE_PREFIX = "hit: safe log: ";
60

61
    private Logger logger = LoggerFactory.getLogger(this.getClass());
1✔
62

63
    private final KeyUtility keyUtility;
64
    protected final AtomicLong checkedKeys = new AtomicLong();
1✔
65
    protected final AtomicLong checkedKeysSumOfTimeToCheckContains = new AtomicLong();
1✔
66
    protected final AtomicLong emptyConsumer = new AtomicLong();
1✔
67
    protected final AtomicLong hits = new AtomicLong();
1✔
68
    protected long startTime = 0;
1✔
69

70
    protected final CConsumerJava consumerJava;
71
    @VisibleForTesting
1✔
72
    ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
1✔
73

74
    protected Persistence persistence;
75
    private final PersistenceUtils persistenceUtils;
76
    
77
    private final List<Future<Void>> consumers = new ArrayList<>();
1✔
78
    protected final LinkedBlockingQueue<PublicKeyBytes[]> keysQueue;
79
    private final ByteBufferUtility byteBufferUtility = new ByteBufferUtility(true);
1✔
80
    
81
    protected final AtomicLong vanityHits = new AtomicLong();
1✔
82
    private final Pattern vanityPattern;
83
    
84
    @VisibleForTesting
1✔
85
    final AtomicBoolean shouldRun = new AtomicBoolean(true);
86
    
87
    @VisibleForTesting
88
    final ExecutorService consumeKeysExecutorService;
89

90
    protected ConsumerJava(CConsumerJava consumerJava, KeyUtility keyUtility, PersistenceUtils persistenceUtils) {
1✔
91
        this.consumerJava = consumerJava;
1✔
92
        this.keysQueue = new LinkedBlockingQueue<>(consumerJava.queueSize);
1✔
93
        this.keyUtility = keyUtility;
1✔
94
        this.persistenceUtils = persistenceUtils;
1✔
95
        if (consumerJava.enableVanity) {
1✔
96
            this.vanityPattern = Pattern.compile(consumerJava.vanityPattern);
1✔
97
        } else {
98
            vanityPattern = null;
1✔
99
        }
100
        consumeKeysExecutorService = Executors.newFixedThreadPool(consumerJava.threads);
1✔
101
    }
1✔
102

103
    Logger getLogger() {
104
        return logger;
×
105
    }
106
    
107
    void setLogger(Logger logger) {
108
        this.logger = logger;
1✔
109
    }
1✔
110

111
    protected void initLMDB() {
112
        persistence = new LMDBPersistence(consumerJava.lmdbConfigurationReadOnly, persistenceUtils);
1✔
113
        persistence.init();
1✔
114
    }
1✔
115

116
    protected void startStatisticsTimer() {
117
        long period = consumerJava.printStatisticsEveryNSeconds;
1✔
118
        if (period <= 0) {
1✔
119
            throw new IllegalArgumentException("period must be greater than 0.");
1✔
120
        }
121

122
        startTime = System.currentTimeMillis();
1✔
123

124
        scheduledExecutorService.scheduleAtFixedRate(() -> {
1✔
125
            // get transient information
126
            long uptime = Math.max(System.currentTimeMillis() - startTime, 1);
1✔
127

128
            String message = new Statistics().createStatisticsMessage(uptime, checkedKeys.get(), checkedKeysSumOfTimeToCheckContains.get(), emptyConsumer.get(), keysQueue.size(), hits.get());
1✔
129

130
            // log the information
131
            logger.info(message);
1✔
132
        }, period, period, TimeUnit.SECONDS);
1✔
133
    }
1✔
134

135
    @Override
136
    public void startConsumer() {
137
        logger.debug("Starting {} consumer threads...", consumerJava.threads);
1✔
138
        for (int i = 0; i < consumerJava.threads; i++) {
1✔
139
            consumers.add(consumeKeysExecutorService.submit(
1✔
140
                    () -> {
141
                        consumeKeysRunner();
1✔
142
                        return null;
1✔
143
                    }));
144
        }
145
        logger.debug("Successfully started {} consumer threads.", consumers.size());
1✔
146
    }
1✔
147
    
148
    /**
149
     * This method runs in multiple threads.
150
     */
151
    private void consumeKeysRunner() {
152
        logger.info("start consumeKeysRunner");
1✔
153
        ByteBuffer threadLocalReuseableByteBuffer = ByteBuffer.allocateDirect(PublicKeyBytes.HASH160_SIZE);
1✔
154
        
155
        while (shouldRun.get()) {
1✔
156
            if (keysQueue.size() >= consumerJava.queueSize) {
1✔
157
                logger.warn("Attention, queue is full. Please increase queue size.");
1✔
158
            }
159
            try {
160
                consumeKeys(threadLocalReuseableByteBuffer);
1✔
161
                // the consumeKeys method is looped inside, if the method returns it means the queue is empty
162
                emptyConsumer.incrementAndGet();
1✔
163
                Thread.sleep(consumerJava.delayEmptyConsumer);
1✔
164
            } catch (InterruptedException e) {
×
165
                // we need to catch the exception to not break the thread
166
                logger.error("Ignore InterruptedException during Thread.sleep.", e);
×
167
            } catch (Exception e) {
×
168
                // log every Exception because it's hard to debug and we do not break down the thread loop
169
                logger.error("Error in consumeKeysRunner()." , e);
×
170
                e.printStackTrace();
×
171
            }
1✔
172
        }
173
        
174
        if (threadLocalReuseableByteBuffer != null) {
1✔
175
            byteBufferUtility.freeByteBuffer(threadLocalReuseableByteBuffer);
1✔
176
        }
177
        logger.info("end consumeKeysRunner");
1✔
178
    }
1✔
179
    
180
    void consumeKeys(ByteBuffer threadLocalReuseableByteBuffer) throws MnemonicException.MnemonicLengthException {
181
        logger.trace("consumeKeys");
1✔
182
        PublicKeyBytes[] publicKeyBytesArray = keysQueue.poll();
1✔
183
        while (publicKeyBytesArray != null) {
1✔
184
            for (PublicKeyBytes publicKeyBytes : publicKeyBytesArray) {
1✔
185
                if (publicKeyBytes.isOutsidePrivateKeyRange()) {
1✔
186
                    continue;
1✔
187
                }
188
                
189
                byte[] hash160Uncompressed = publicKeyBytes.getUncompressedKeyHash();
1✔
190
                boolean containsAddressUncompressed = containsAddress(threadLocalReuseableByteBuffer, hash160Uncompressed);
1✔
191
                
192
                byte[] hash160Compressed = publicKeyBytes.getCompressedKeyHash();
1✔
193
                boolean containsAddressCompressed = containsAddress(threadLocalReuseableByteBuffer, hash160Compressed);
1✔
194
                
195
                if (consumerJava.runtimePublicKeyCalculationCheck) {
1✔
196
                    publicKeyBytes.runtimePublicKeyCalculationCheck(logger);
1✔
197
                }
198
                
199
                if (containsAddressUncompressed) {
1✔
200
                    // immediately log the secret
201
                    safeLog(publicKeyBytes, hash160Uncompressed, hash160Compressed);
1✔
202
                    hits.incrementAndGet();
1✔
203
                    ECKey ecKeyUncompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getUncompressed());
1✔
204
                    String hitMessageUncompressed = HIT_PREFIX + keyUtility.createKeyDetails(ecKeyUncompressed);
1✔
205
                    logger.info(hitMessageUncompressed);
1✔
206
                }
207

208
                if (containsAddressCompressed) {
1✔
209
                    // immediately log the secret
210
                    safeLog(publicKeyBytes, hash160Uncompressed, hash160Compressed);
1✔
211
                    hits.incrementAndGet();
1✔
212
                    ECKey ecKeyCompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getCompressed());
1✔
213
                    String hitMessageCompressed = HIT_PREFIX + keyUtility.createKeyDetails(ecKeyCompressed);
1✔
214
                    logger.info(hitMessageCompressed);
1✔
215
                }
216

217
                if (consumerJava.enableVanity) {
1✔
218
                    String uncompressedKeyHashAsBase58 = publicKeyBytes.getUncompressedKeyHashAsBase58(keyUtility);
1✔
219
                    Matcher uncompressedKeyHashAsBase58Matcher = vanityPattern.matcher(uncompressedKeyHashAsBase58);
1✔
220
                    if (uncompressedKeyHashAsBase58Matcher.matches()) {
1✔
221
                        // immediately log the secret
222
                        safeLog(publicKeyBytes, hash160Uncompressed, hash160Compressed);
1✔
223
                        vanityHits.incrementAndGet();
1✔
224
                        ECKey ecKeyUncompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getUncompressed());
1✔
225
                        String vanityHitMessageUncompressed = VANITY_HIT_PREFIX + keyUtility.createKeyDetails(ecKeyUncompressed);
1✔
226
                        logger.info(vanityHitMessageUncompressed);
1✔
227
                    }
228

229
                    String compressedKeyHashAsBase58 = publicKeyBytes.getCompressedKeyHashAsBase58(keyUtility);
1✔
230
                    Matcher compressedKeyHashAsBase58Matcher = vanityPattern.matcher(compressedKeyHashAsBase58);
1✔
231
                    if (compressedKeyHashAsBase58Matcher.matches()) {
1✔
232
                        // immediately log the secret
233
                        safeLog(publicKeyBytes, hash160Uncompressed, hash160Compressed);
1✔
234
                        vanityHits.incrementAndGet();
1✔
235
                        ECKey ecKeyCompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getCompressed());
1✔
236
                        String vanityHitMessageCompressed = VANITY_HIT_PREFIX + keyUtility.createKeyDetails(ecKeyCompressed);
1✔
237
                        logger.info(vanityHitMessageCompressed);
1✔
238
                    }
239
                }
240

241
                if (!containsAddressUncompressed && !containsAddressCompressed) {
1✔
242
                    if (logger.isTraceEnabled()) {
1✔
243
                        ECKey ecKeyUncompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getUncompressed());
1✔
244
                        String missMessageUncompressed = MISS_PREFIX + keyUtility.createKeyDetails(ecKeyUncompressed);
1✔
245
                        logger.trace(missMessageUncompressed);
1✔
246

247
                        ECKey ecKeyCompressed = ECKey.fromPrivateAndPrecalculatedPublic(publicKeyBytes.getSecretKey().toByteArray(), publicKeyBytes.getCompressed());
1✔
248
                        String missMessageCompressed = MISS_PREFIX + keyUtility.createKeyDetails(ecKeyCompressed);
1✔
249
                        logger.trace(missMessageCompressed);
1✔
250
                    }
251
                }
252
            }
253
            publicKeyBytesArray = keysQueue.poll();
1✔
254
        }
255
    }
1✔
256

257
    public boolean containsAddress(ByteBuffer threadLocalReuseableByteBuffer, byte[] hash160) {
258
        threadLocalReuseableByteBuffer.rewind();
1✔
259
        threadLocalReuseableByteBuffer.put(hash160);
1✔
260
        threadLocalReuseableByteBuffer.flip();
1✔
261
        return containsAddress(threadLocalReuseableByteBuffer);
1✔
262
    }
263
    
264
    /**
265
     * Try to log safe informations which may not thrown an exception.
266
     */
267
    private void safeLog(PublicKeyBytes publicKeyBytes, byte[] hash160Uncompressed, byte[] hash160Compressed) {
268
        logger.info(HIT_SAFE_PREFIX +"publicKeyBytes.getSecretKey(): " + publicKeyBytes.getSecretKey());
1✔
269
        logger.info(HIT_SAFE_PREFIX +"publicKeyBytes.getUncompressed(): " + Hex.encodeHexString(publicKeyBytes.getUncompressed()));
1✔
270
        logger.info(HIT_SAFE_PREFIX +"publicKeyBytes.getCompressed(): " + Hex.encodeHexString(publicKeyBytes.getCompressed()));
1✔
271
        logger.info(HIT_SAFE_PREFIX +"hash160Uncompressed: " + Hex.encodeHexString(hash160Uncompressed));
1✔
272
        logger.info(HIT_SAFE_PREFIX +"hash160Compressed: " + Hex.encodeHexString(hash160Compressed));
1✔
273
    }
1✔
274

275
    private boolean containsAddress(ByteBuffer hash160AsByteBuffer) {
276
        long timeBefore = System.currentTimeMillis();
1✔
277
        if (logger.isTraceEnabled()) {
1✔
278
            logger.trace("Time before persistence.containsAddress: " + timeBefore);
1✔
279
        }
280
        boolean containsAddress = persistence.containsAddress(hash160AsByteBuffer);
1✔
281
        long timeAfter = System.currentTimeMillis();
1✔
282
        long timeDelta = timeAfter - timeBefore;
1✔
283
        checkedKeys.incrementAndGet();
1✔
284
        checkedKeysSumOfTimeToCheckContains.addAndGet(timeDelta);
1✔
285
        if (logger.isTraceEnabled()) {
1✔
286
            logger.trace("Time after persistence.containsAddress: " + timeAfter);
1✔
287
            logger.trace("Time delta: " + timeDelta);
1✔
288
        }
289
        return containsAddress;
1✔
290
    }
291

292
    @Override
293
    public void consumeKeys(PublicKeyBytes[] publicKeyBytes) throws InterruptedException {
294
        if(logger.isDebugEnabled()){
1✔
295
            logger.debug("keysQueue.put(publicKeyBytes) with length: " + publicKeyBytes.length);
1✔
296
        }
297
        
298
        keysQueue.put(publicKeyBytes);
1✔
299
        
300
        if(logger.isDebugEnabled()){
1✔
301
            logger.debug("keysQueue.size(): " + keysQueue.size());
1✔
302
        }
303
    }
1✔
304
    
305
    @Override
306
    public void interrupt() {
307
        logger.debug("Interrupt initiated: stopping consumer execution...");
1✔
308
        shouldRun.set(false);
1✔
309
        scheduledExecutorService.shutdown();
1✔
310
        consumeKeysExecutorService.shutdown();
1✔
311
        logger.debug("Waiting for termination of {} consumer threads (timeout: {} seconds)...", consumers.size(), AWAIT_DURATION_QUEUE_EMPTY.getSeconds());
1✔
312
        try {
313
            boolean terminated = consumeKeysExecutorService.awaitTermination(AWAIT_DURATION_QUEUE_EMPTY.get(ChronoUnit.SECONDS), TimeUnit.SECONDS);
1✔
314
            if (!terminated) {
1✔
UNCOV
315
                logger.warn("Timeout reached. Some consumer threads may not have terminated cleanly.");
×
316
            }
317
        } catch (InterruptedException ex) {
×
318
            logger.error("Interrupted while awaiting consumer termination.", ex);
×
319
            throw new RuntimeException(ex);
×
320
        }
1✔
321
        persistence.close();
1✔
322
        logger.debug("Interrupt complete: resources released and persistence closed.");
1✔
323
    }
1✔
324
    
325
    @VisibleForTesting
326
    int keysQueueSize() {
327
        return keysQueue.size();
1✔
328
    }
329
    
330
    @Override
331
    public String toString() {
332
        return "ConsumerJava@" + Integer.toHexString(System.identityHashCode(this));
1✔
333
    }
334
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc