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

bernardladenthin / BitcoinAddressFinder / #344

13 Jul 2025 09:44PM UTC coverage: 69.604% (+2.2%) from 67.389%
#344

push

web-flow
Merge pull request #61 from bernardladenthin/develop

Merge bloom filter

30 of 31 new or added lines in 3 files covered. (96.77%)

2 existing lines in 1 file now uncovered.

1406 of 2020 relevant lines covered (69.6%)

0.7 hits per line

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

84.0
/src/main/java/net/ladenthin/bitcoinaddressfinder/persistence/lmdb/LMDBPersistence.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.persistence.lmdb;
20

21
import com.google.common.hash.BloomFilter;
22
import com.google.common.hash.Funnels;
23
import net.ladenthin.bitcoinaddressfinder.persistence.Persistence;
24
import net.ladenthin.bitcoinaddressfinder.persistence.PersistenceUtils;
25
import org.lmdbjava.CursorIterable;
26
import org.lmdbjava.Dbi;
27
import org.lmdbjava.Env;
28
import org.lmdbjava.EnvFlags;
29
import org.lmdbjava.KeyRange;
30
import org.lmdbjava.Txn;
31

32
import java.io.File;
33
import java.io.FileWriter;
34
import java.io.IOException;
35
import java.nio.ByteBuffer;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.concurrent.atomic.AtomicBoolean;
39
import net.ladenthin.bitcoinaddressfinder.ByteBufferUtility;
40
import net.ladenthin.bitcoinaddressfinder.ByteConversion;
41
import net.ladenthin.bitcoinaddressfinder.KeyUtility;
42
import net.ladenthin.bitcoinaddressfinder.SeparatorFormat;
43
import net.ladenthin.bitcoinaddressfinder.configuration.CAddressFileOutputFormat;
44
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationReadOnly;
45
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationWrite;
46
import org.apache.commons.codec.binary.Hex;
47
import org.bitcoinj.base.Coin;
48
import org.bitcoinj.base.LegacyAddress;
49
import org.lmdbjava.BufferProxy;
50
import org.lmdbjava.ByteBufferProxy;
51

52
import static org.lmdbjava.DbiFlags.MDB_CREATE;
53
import static org.lmdbjava.Env.create;
54
import org.lmdbjava.EnvInfo;
55
import org.slf4j.Logger;
56
import org.slf4j.LoggerFactory;
57

58
public class LMDBPersistence implements Persistence {
59

60
    private static final String DB_NAME_HASH160_TO_COINT = "hash160toCoin";
61
    private static final int DB_COUNT = 1;
62
    
63
    private final Logger logger = LoggerFactory.getLogger(LMDBPersistence.class);
1✔
64

65
    private final PersistenceUtils persistenceUtils;
66
    private final CLMDBConfigurationWrite lmdbConfigurationWrite;
67
    private final CLMDBConfigurationReadOnly lmdbConfigurationReadOnly;
68
    private final KeyUtility keyUtility;
69
    private Env<ByteBuffer> env;
70
    private Dbi<ByteBuffer> lmdb_h160ToAmount;
71
    private long increasedCounter = 0;
1✔
72
    private long increasedSum = 0;
1✔
73
    private BloomFilter<byte[]> addressBloomFilter = null;
1✔
74

75

76
    public LMDBPersistence(CLMDBConfigurationWrite lmdbConfigurationWrite, PersistenceUtils persistenceUtils) {
1✔
77
        this.lmdbConfigurationReadOnly = null;
1✔
78
        this.lmdbConfigurationWrite = lmdbConfigurationWrite;
1✔
79
        this.persistenceUtils = persistenceUtils;
1✔
80
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
81
    }
1✔
82

83
    public LMDBPersistence(CLMDBConfigurationReadOnly lmdbConfigurationReadOnly, PersistenceUtils persistenceUtils) {
1✔
84
        this.lmdbConfigurationReadOnly = lmdbConfigurationReadOnly;
1✔
85
        lmdbConfigurationWrite = null;
1✔
86
        this.persistenceUtils = persistenceUtils;
1✔
87
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
88
    }
1✔
89
    
90
    @Override
91
    public void init() {
92
        if (lmdbConfigurationWrite != null) {
1✔
93
            initWritable();
1✔
94
        } else if (lmdbConfigurationReadOnly != null) {
1✔
95
            initReadOnly();
1✔
96
        } else {
97
            throw new IllegalArgumentException("Neither write nor read-only configuration provided.");
×
98
        }
99
        
100
        
101
        logStatsIfConfigured(true);
1✔
102
    }
1✔
103
    
104
    public void buildAddressBloomFilter() {
105
        logger.info("##### BEGIN: buildAddressBloomFilter #####");
1✔
106
        // Attention: slow!
107
        long count = count();
1✔
108

109
        BloomFilter<byte[]> filter = BloomFilter.create(Funnels.byteArrayFunnel(), count, lmdbConfigurationReadOnly.bloomFilterFpp);
1✔
110
        long inserted = 0;
1✔
111

112
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
113
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
114
                for (CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
115
                    ByteBuffer key = kv.key();
1✔
116
                    byte[] keyBytes = new byte[key.remaining()];
1✔
117
                    key.get(keyBytes);
1✔
118
                    key.rewind();
1✔
119
                    filter.put(keyBytes);
1✔
120
                    inserted++;
1✔
121
                }
1✔
122
            }
123
        }
124

125
        addressBloomFilter = filter;
1✔
126
        logger.info("Inserted {} addresses into BloomFilter", inserted);
1✔
127
        logger.info("##### END: buildAddressBloomFilter #####");
1✔
128
    }
1✔
129

130
    public void unloadBloomFilter() {
NEW
131
        addressBloomFilter = null;
×
UNCOV
132
    }
×
133
    
134
    private void initReadOnly() {
135
        BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationReadOnly.useProxyOptimal);
1✔
136
        env = create(bufferProxy).setMaxDbs(DB_COUNT).open(new File(lmdbConfigurationReadOnly.lmdbDirectory), EnvFlags.MDB_RDONLY_ENV, EnvFlags.MDB_NOLOCK);
1✔
137
        lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT);
1✔
138
        
139
        if (lmdbConfigurationReadOnly.useBloomFilter) {
1✔
140
            buildAddressBloomFilter();
1✔
141
        }
142
    }
1✔
143

144
    private void initWritable() {
145
        // -Xmx10G -XX:MaxDirectMemorySize=5G
146
        // We always need an Env. An Env owns a physical on-disk storage file. One
147
        // Env can store many different databases (ie sorted maps).
148
        File lmdbDirectory = new File(lmdbConfigurationWrite.lmdbDirectory);
1✔
149
        lmdbDirectory.mkdirs();
1✔
150
        
151
        BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationWrite.useProxyOptimal);
1✔
152
        
153
        env = create(bufferProxy)
1✔
154
                // LMDB also needs to know how large our DB might be. Over-estimating is OK.
155
                .setMapSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.initialMapSizeInMiB))
1✔
156
                // LMDB also needs to know how many DBs (Dbi) we want to store in this Env.
157
                .setMaxDbs(DB_COUNT)
1✔
158
                // Now let's open the Env. The same path can be concurrently opened and
159
                // used in different processes, but do not open the same path twice in
160
                // the same process at the same time.
161
                
162
                //https://github.com/kentnl/CHI-Driver-LMDB
163
                .open(lmdbDirectory, EnvFlags.MDB_NOSYNC, EnvFlags.MDB_NOMETASYNC, EnvFlags.MDB_WRITEMAP, EnvFlags.MDB_MAPASYNC);
1✔
164
        // We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The
165
        // MDB_CREATE flag causes the DB to be created if it doesn't already exist.
166
        lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT, MDB_CREATE);
1✔
167
    }
1✔
168

169
    /**
170
     * https://github.com/lmdbjava/lmdbjava/wiki/Buffers
171
     *
172
     * @param useProxyOptimal
173
     * @return
174
     */
175
    private BufferProxy<ByteBuffer> getBufferProxyByUseProxyOptimal(boolean useProxyOptimal) {
176
        if (useProxyOptimal) {
1✔
177
            return ByteBufferProxy.PROXY_OPTIMAL;
1✔
178
        } else {
179
            return ByteBufferProxy.PROXY_SAFE;
×
180
        }
181
    }
182
    
183
    private void logStatsIfConfigured(boolean onInit) {
184
        if (isLoggingEnabled(lmdbConfigurationWrite, onInit) || isLoggingEnabled(lmdbConfigurationReadOnly, onInit)) {
1✔
185
            logStats();
1✔
186
        }
187
    }
1✔
188

189
    private boolean isLoggingEnabled(CLMDBConfigurationReadOnly config, boolean onInit) {
190
        return config != null && (onInit ? config.logStatsOnInit : config.logStatsOnClose);
1✔
191
    }
192

193
    @Override
194
    public void close() {
195
        logStatsIfConfigured(false);
1✔
196
        lmdb_h160ToAmount.close();
1✔
197
        env.close();
1✔
198
    }
1✔
199
    
200
    @Override
201
    public boolean isClosed() {
202
        return env.isClosed();
1✔
203
    }
204

205
    @Override
206
    public Coin getAmount(ByteBuffer hash160) {
207
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
208
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
209
            return getCoinFromByteBuffer(byteBuffer);
1✔
210
        }
211
    }
212
    
213
    private Coin getCoinFromByteBuffer(ByteBuffer byteBuffer) {
214
        if (byteBuffer == null || byteBuffer.capacity() == 0) {
1✔
215
            return Coin.ZERO;
1✔
216
        }
217
        return Coin.valueOf(byteBuffer.getLong());
1✔
218
    }
219

220
    @Override
221
    public boolean containsAddress(ByteBuffer hash160) {
222
        if (lmdbConfigurationReadOnly.disableAddressLookup) {
1✔
UNCOV
223
            return false;
×
224
        }
225
        
226
        byte[] hash160AsByteArray = new byte[hash160.remaining()];
1✔
227
        hash160.get(hash160AsByteArray);
1✔
228
        hash160.rewind();
1✔
229
        
230
        // Use Bloom filter if available for fast pre-check
231
        if (addressBloomFilter != null) {
1✔
232
            boolean mightContain = addressBloomFilter.mightContain(hash160AsByteArray);
1✔
233
            if (!mightContain) {
1✔
234
                return false; // definitely not present
1✔
235
            }
236
            // Possibly in DB, proceed to verify
237
        }
238
        
239
        // Perform LMDB lookup (always happens if no Bloom filter is present)
240
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
241
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
242
            return byteBuffer != null;
1✔
243
        }
244
    }
245

246
    @Override
247
    public void writeAllAmountsToAddressFile(File file, CAddressFileOutputFormat addressFileOutputFormat, AtomicBoolean shouldRun) throws IOException {
248
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
249
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
250
                try (FileWriter writer = new FileWriter(file)) {
1✔
251
                    for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
252
                        if (!shouldRun.get()) {
1✔
253
                            return;
×
254
                        }
255
                        ByteBuffer addressAsByteBuffer = kv.key();
1✔
256
                        if(logger.isTraceEnabled()) {
1✔
257
                            String hexFromByteBuffer = new ByteBufferUtility(false).getHexFromByteBuffer(addressAsByteBuffer);
×
258
                            logger.trace("Process address: " + hexFromByteBuffer);
×
259
                        }
260
                        LegacyAddress address = keyUtility.byteBufferToAddress(addressAsByteBuffer);
1✔
261
                        final String line;
262
                        switch(addressFileOutputFormat) {
1✔
263
                            case HexHash:
264
                                line = Hex.encodeHexString(address.getHash()) + System.lineSeparator();
1✔
265
                                break;
1✔
266
                            case FixedWidthBase58BitcoinAddress:
267
                                line = String.format("%-34s", address.toBase58()) + System.lineSeparator();
1✔
268
                                break;
1✔
269
                            case DynamicWidthBase58BitcoinAddressWithAmount:
270
                                ByteBuffer value = kv.val();
1✔
271
                                Coin coin = getCoinFromByteBuffer(value);
1✔
272
                                line = address.toBase58() + SeparatorFormat.COMMA.getSymbol() + coin.getValue() + System.lineSeparator();
1✔
273
                                break;
1✔
274
                            default:
275
                                throw new IllegalArgumentException("Unknown addressFileOutputFormat: " + addressFileOutputFormat);
×
276
                        }
277
                        writer.write(line);
1✔
278
                    }
1✔
279
                }
×
280
            }
×
281
        }
×
282
    }
1✔
283

284
    @Override
285
    public void putAllAmounts(Map<ByteBuffer, Coin> amounts) throws IOException {
286
        for (Map.Entry<ByteBuffer, Coin> entry : amounts.entrySet()) {
×
287
            ByteBuffer hash160 = entry.getKey();
×
288
            Coin coin = entry.getValue();
×
289
            putNewAmount(hash160, coin);
×
290
        }
×
291
    }
×
292

293
    @Override
294
    public void changeAmount(ByteBuffer hash160, Coin amountToChange) {
295
        Coin valueInDB = getAmount(hash160);
×
296
        Coin toWrite = valueInDB.add(amountToChange);
×
297
        putNewAmount(hash160, toWrite);
×
298
    }
×
299

300
    @Override
301
    public void putNewAmount(ByteBuffer hash160, Coin amount) {
302
        putNewAmountWithAutoIncrease(hash160, amount);
1✔
303
    }
1✔
304
    
305
    /**
306
     * If an {@link org.lmdbjava.Env.MapFullException} was thrown during a put. The map might be increased if configured.
307
     * The increase value needs to be high enough. Otherwise the next put fails nevertheless.
308
     */
309
    private void putNewAmountWithAutoIncrease(ByteBuffer hash160, Coin amount) {
310
        try {
311
            putNewAmountUnsafe(hash160, amount);
1✔
312
        } catch (org.lmdbjava.Env.MapFullException e) {
1✔
313
            if (lmdbConfigurationWrite.increaseMapAutomatically == true) {
1✔
314
                increaseDatabaseSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.increaseSizeInMiB));
1✔
315
                /**
316
                 * It is possible that the exception will be thrown again, in this case increaseSizeInMiB should be changed and it's a configuration issue.
317
                 * See {@link CLMDBConfigurationWrite#increaseSizeInMiB}.
318
                 */
319
                putNewAmountUnsafe(hash160, amount);
1✔
320
            } else {
321
                throw e;
1✔
322
            }
323
        }
1✔
324
    }
1✔
325
    
326
    private void putNewAmountUnsafe(ByteBuffer hash160, Coin amount) {
327
        try (Txn<ByteBuffer> txn = env.txnWrite()) {
1✔
328
            if (lmdbConfigurationWrite.deleteEmptyAddresses && amount.isZero()) {
1✔
329
                lmdb_h160ToAmount.delete(txn, hash160);
×
330
            } else {
331
                long amountAsLong = amount.longValue();
1✔
332
                if (lmdbConfigurationWrite.useStaticAmount) {
1✔
333
                    amountAsLong = lmdbConfigurationWrite.staticAmount;
1✔
334
                }
335
                lmdb_h160ToAmount.put(txn, hash160, persistenceUtils.longToByteBufferDirect(amountAsLong));
1✔
336
            }
337
            txn.commit();
1✔
338
        }
339
    }
1✔
340

341
    @Override
342
    public Coin getAllAmountsFromAddresses(List<ByteBuffer> hash160s) {
343
        Coin allAmounts = Coin.ZERO;
×
344
        for (ByteBuffer hash160 : hash160s) {
×
345
            allAmounts = allAmounts.add(getAmount(hash160));
×
346
        }
×
347
        return allAmounts;
×
348
    }
349

350
    @Override
351
    public long count() {
352
        long count = 0;
1✔
353
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
354
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
355
                for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
356
                    count++;
1✔
357
                }
1✔
358
            }
359
        }
360
        return count;
1✔
361
    }
362

363
    @Override
364
    public long getDatabaseSize() {
365
        EnvInfo info = env.info();
1✔
366
        return info.mapSize;
1✔
367
    }
368

369
    @Override
370
    public void increaseDatabaseSize(long toIncrease) {
371
        increasedCounter++;
1✔
372
        increasedSum += toIncrease;
1✔
373
        long newSize = getDatabaseSize() + toIncrease;
1✔
374
        env.setMapSize(newSize);
1✔
375
    }
1✔
376

377
    @Override
378
    public long getIncreasedCounter() {
379
        return increasedCounter;
1✔
380
    }
381

382
    @Override
383
    public long getIncreasedSum() {
384
        return increasedSum;
1✔
385
    }
386
    
387
    @Override
388
    public void logStats() {
389
        logger.info("##### BEGIN: LMDB stats #####");
1✔
390
        logger.info("... this may take a lot of time ...");
1✔
391
        logger.info("DatabaseSize: " + new ByteConversion().bytesToMib(getDatabaseSize()) + " MiB");
1✔
392
        logger.info("IncreasedCounter: " + getIncreasedCounter());
1✔
393
        logger.info("IncreasedSum: " + new ByteConversion().bytesToMib(getIncreasedSum()) + " MiB");
1✔
394
        logger.info("Stat: " + env.stat());
1✔
395
        // Attention: slow!
396
        long count = count();
1✔
397
        logger.info("LMDB contains " + count + " unique entries.");
1✔
398
        logger.info("##### END: LMDB stats #####");
1✔
399
    }
1✔
400
}
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