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

bernardladenthin / BitcoinAddressFinder / #255

08 Apr 2025 02:52PM UTC coverage: 64.326%. Remained the same
#255

push

bernardladenthin
Changes for org.bitcoinj 0.17, improve NetworkParameters usage.

25 of 26 new or added lines in 11 files covered. (96.15%)

17 existing lines in 2 files now uncovered.

1145 of 1780 relevant lines covered (64.33%)

0.64 hits per line

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

81.94
/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 net.ladenthin.bitcoinaddressfinder.persistence.Persistence;
22
import net.ladenthin.bitcoinaddressfinder.persistence.PersistenceUtils;
23
import org.lmdbjava.CursorIterable;
24
import org.lmdbjava.Dbi;
25
import org.lmdbjava.Env;
26
import org.lmdbjava.EnvFlags;
27
import org.lmdbjava.KeyRange;
28
import org.lmdbjava.Txn;
29

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

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

56
public class LMDBPersistence implements Persistence {
57

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

63
    private final PersistenceUtils persistenceUtils;
64
    private final CLMDBConfigurationWrite lmdbConfigurationWrite;
65
    private final CLMDBConfigurationReadOnly lmdbConfigurationReadOnly;
66
    private final KeyUtility keyUtility;
67
    private Env<ByteBuffer> env;
68
    private Dbi<ByteBuffer> lmdb_h160ToAmount;
69
    private long increasedCounter = 0;
1✔
70
    private long increasedSum = 0;
1✔
71

72
    public LMDBPersistence(CLMDBConfigurationWrite lmdbConfigurationWrite, PersistenceUtils persistenceUtils) {
1✔
73
        this.lmdbConfigurationReadOnly = null;
1✔
74
        this.lmdbConfigurationWrite = lmdbConfigurationWrite;
1✔
75
        this.persistenceUtils = persistenceUtils;
1✔
76
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
77
    }
1✔
78

79
    public LMDBPersistence(CLMDBConfigurationReadOnly lmdbConfigurationReadOnly, PersistenceUtils persistenceUtils) {
1✔
80
        this.lmdbConfigurationReadOnly = lmdbConfigurationReadOnly;
1✔
81
        lmdbConfigurationWrite = null;
1✔
82
        this.persistenceUtils = persistenceUtils;
1✔
83
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
84
    }
1✔
85
    
86
    @Override
87
    public void init() {
88
        if (lmdbConfigurationWrite != null) {
1✔
89
            // -Xmx10G -XX:MaxDirectMemorySize=5G
90
            // We always need an Env. An Env owns a physical on-disk storage file. One
91
            // Env can store many different databases (ie sorted maps).
92
            File lmdbDirectory = new File(lmdbConfigurationWrite.lmdbDirectory);
1✔
93
            lmdbDirectory.mkdirs();
1✔
94

95
            BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationWrite.useProxyOptimal);
1✔
96

97
            env = create(bufferProxy)
1✔
98
                    // LMDB also needs to know how large our DB might be. Over-estimating is OK.
99
                    .setMapSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.initialMapSizeInMiB))
1✔
100
                    // LMDB also needs to know how many DBs (Dbi) we want to store in this Env.
101
                    .setMaxDbs(DB_COUNT)
1✔
102
                    // Now let's open the Env. The same path can be concurrently opened and
103
                    // used in different processes, but do not open the same path twice in
104
                    // the same process at the same time.
105

106
                    //https://github.com/kentnl/CHI-Driver-LMDB
107
                    .open(lmdbDirectory, EnvFlags.MDB_NOSYNC, EnvFlags.MDB_NOMETASYNC, EnvFlags.MDB_WRITEMAP, EnvFlags.MDB_MAPASYNC);
1✔
108
            // We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The
109
            // MDB_CREATE flag causes the DB to be created if it doesn't already exist.
110
            lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT, MDB_CREATE);
1✔
111
        } else if (lmdbConfigurationReadOnly != null) {
1✔
112
            BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationReadOnly.useProxyOptimal);
1✔
113
            env = create(bufferProxy).setMaxDbs(DB_COUNT).open(new File(lmdbConfigurationReadOnly.lmdbDirectory), EnvFlags.MDB_RDONLY_ENV, EnvFlags.MDB_NOLOCK);
1✔
114
            lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT);
1✔
115
        } else {
1✔
UNCOV
116
            throw new IllegalArgumentException();
×
117
        }
118
        
119
        logStatsOnInitByConfig();
1✔
120
    }
1✔
121

122
    /**
123
     * https://github.com/lmdbjava/lmdbjava/wiki/Buffers
124
     *
125
     * @param useProxyOptimal
126
     * @return
127
     */
128
    private BufferProxy<ByteBuffer> getBufferProxyByUseProxyOptimal(boolean useProxyOptimal) {
129
        if (useProxyOptimal) {
1✔
130
            return ByteBufferProxy.PROXY_OPTIMAL;
1✔
131
        } else {
UNCOV
132
            return ByteBufferProxy.PROXY_SAFE;
×
133
        }
134
    }
135
    
136
    private void logStatsOnInitByConfig() {
137
        if (lmdbConfigurationWrite != null) {
1✔
138
            if (lmdbConfigurationWrite.logStatsOnInit) {
1✔
139
                logStats();
1✔
140
            }
141
        }
142
        if (lmdbConfigurationReadOnly != null) {
1✔
143
            if (lmdbConfigurationReadOnly.logStatsOnInit) {
1✔
UNCOV
144
                logStats();
×
145
            }
146
        }
147
    }
1✔
148
    
149
    private void logStatsOnCloseByConfig() {
150
        if (lmdbConfigurationWrite != null) {
1✔
151
            if (lmdbConfigurationWrite.logStatsOnClose) {
1✔
152
                logStats();
1✔
153
            }
154
        }
155
        if (lmdbConfigurationReadOnly != null) {
1✔
156
            if (lmdbConfigurationReadOnly.logStatsOnClose) {
1✔
UNCOV
157
                logStats();
×
158
            }
159
        }
160
    }
1✔
161

162
    @Override
163
    public void close() {
164
        logStatsOnCloseByConfig();
1✔
165
        lmdb_h160ToAmount.close();
1✔
166
        env.close();
1✔
167
    }
1✔
168
    
169
    @Override
170
    public boolean isClosed() {
171
        return env.isClosed();
1✔
172
    }
173

174
    @Override
175
    public Coin getAmount(ByteBuffer hash160) {
176
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
177
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
178
            txn.close();
1✔
179

180
            return getCoinFromByteBuffer(byteBuffer);
1✔
181
        }
182
    }
183
    
184
    private Coin getCoinFromByteBuffer(ByteBuffer byteBuffer) {
185
        if (byteBuffer != null) {
1✔
186
            if (byteBuffer.capacity() == 0) {
1✔
187
                return Coin.ZERO;
1✔
188
            } else {
189
                return Coin.valueOf(byteBuffer.getLong());
1✔
190
            }
191
        } else {
UNCOV
192
            return Coin.ZERO;
×
193
        }
194
    }
195

196
    @Override
197
    public boolean containsAddress(ByteBuffer hash160) {
198
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
199
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
200
            txn.close();
1✔
201
            return byteBuffer != null;
1✔
202
        }
203
    }
204

205
    @Override
206
    public void writeAllAmountsToAddressFile(File file, CAddressFileOutputFormat addressFileOutputFormat, AtomicBoolean shouldRun) throws IOException {
207
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
208
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
209
                try (FileWriter writer = new FileWriter(file)) {
1✔
210
                    for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
211
                        if (!shouldRun.get()) {
1✔
UNCOV
212
                            return;
×
213
                        }
214
                        ByteBuffer addressAsByteBuffer = kv.key();
1✔
215
                        if(logger.isTraceEnabled()) {
1✔
UNCOV
216
                            String hexFromByteBuffer = new ByteBufferUtility(false).getHexFromByteBuffer(addressAsByteBuffer);
×
217
                            logger.trace("Process address: " + hexFromByteBuffer);
×
218
                        }
219
                        LegacyAddress address = keyUtility.byteBufferToAddress(addressAsByteBuffer);
1✔
220
                        final String line;
221
                        switch(addressFileOutputFormat) {
1✔
222
                            case HexHash:
223
                                line = Hex.encodeHexString(address.getHash()) + System.lineSeparator();
1✔
224
                                break;
1✔
225
                            case FixedWidthBase58BitcoinAddress:
226
                                line = String.format("%-34s", address.toBase58()) + System.lineSeparator();
1✔
227
                                break;
1✔
228
                            case DynamicWidthBase58BitcoinAddressWithAmount:
229
                                ByteBuffer value = kv.val();
1✔
230
                                Coin coin = getCoinFromByteBuffer(value);
1✔
231
                                line = address.toBase58() + SeparatorFormat.COMMA.getSymbol() + coin.getValue() + System.lineSeparator();
1✔
232
                                break;
1✔
233
                            default:
UNCOV
234
                                throw new IllegalArgumentException("Unknown addressFileOutputFormat: " + addressFileOutputFormat);
×
235
                        }
236
                        writer.write(line);
1✔
237
                    }
1✔
UNCOV
238
                }
×
239
            }
×
240
        }
×
241
    }
1✔
242

243
    @Override
244
    public void putAllAmounts(Map<ByteBuffer, Coin> amounts) throws IOException {
UNCOV
245
        for (Map.Entry<ByteBuffer, Coin> entry : amounts.entrySet()) {
×
246
            ByteBuffer hash160 = entry.getKey();
×
247
            Coin coin = entry.getValue();
×
248
            putNewAmount(hash160, coin);
×
249
        }
×
250
    }
×
251

252
    @Override
253
    public void changeAmount(ByteBuffer hash160, Coin amountToChange) {
UNCOV
254
        Coin valueInDB = getAmount(hash160);
×
255
        Coin toWrite = valueInDB.add(amountToChange);
×
256
        putNewAmount(hash160, toWrite);
×
257
    }
×
258

259
    @Override
260
    public void putNewAmount(ByteBuffer hash160, Coin amount) {
261
        putNewAmountWithAutoIncrease(hash160, amount);
1✔
262
    }
1✔
263
    
264
    /**
265
     * If an {@link org.lmdbjava.Env.MapFullException} was thrown during a put. The map might be increased if configured.
266
     * The increase value needs to be high enough. Otherwise the next put fails nevertheless.
267
     */
268
    private void putNewAmountWithAutoIncrease(ByteBuffer hash160, Coin amount) {
269
        try {
270
            putNewAmountUnsafe(hash160, amount);
1✔
271
        } catch (org.lmdbjava.Env.MapFullException e) {
1✔
272
            if (lmdbConfigurationWrite.increaseMapAutomatically == true) {
1✔
273
                increaseDatabaseSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.increaseSizeInMiB));
1✔
274
                /**
275
                 * It is possible that the exception will be thrown again, in this case increaseSizeInMiB should be changed and it's a configuration issue.
276
                 * See {@link CLMDBConfigurationWrite#increaseSizeInMiB}.
277
                 */
278
                putNewAmountUnsafe(hash160, amount);
1✔
279
            } else {
280
                throw e;
1✔
281
            }
282
        }
1✔
283
    }
1✔
284
    
285
    private void putNewAmountUnsafe(ByteBuffer hash160, Coin amount) {
286
        try (Txn<ByteBuffer> txn = env.txnWrite()) {
1✔
287
            if (lmdbConfigurationWrite.deleteEmptyAddresses && amount.isZero()) {
1✔
UNCOV
288
                lmdb_h160ToAmount.delete(txn, hash160);
×
289
            } else {
290
                long amountAsLong = amount.longValue();
1✔
291
                if (lmdbConfigurationWrite.useStaticAmount) {
1✔
292
                    amountAsLong = lmdbConfigurationWrite.staticAmount;
1✔
293
                }
294
                lmdb_h160ToAmount.put(txn, hash160, persistenceUtils.longToByteBufferDirect(amountAsLong));
1✔
295
            }
296
            txn.commit();
1✔
297
            txn.close();
1✔
298
        }
299
    }
1✔
300

301
    @Override
302
    public Coin getAllAmountsFromAddresses(List<ByteBuffer> hash160s) {
UNCOV
303
        Coin allAmounts = Coin.ZERO;
×
304
        for (ByteBuffer hash160 : hash160s) {
×
305
            allAmounts = allAmounts.add(getAmount(hash160));
×
306
        }
×
307
        return allAmounts;
×
308
    }
309

310
    @Override
311
    public long count() {
312
        long count = 0;
1✔
313
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
314
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
315
                for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
316
                    count++;
1✔
317
                }
1✔
318
            }
319
        }
320
        return count;
1✔
321
    }
322

323
    @Override
324
    public long getDatabaseSize() {
325
        EnvInfo info = env.info();
1✔
326
        return info.mapSize;
1✔
327
    }
328

329
    @Override
330
    public void increaseDatabaseSize(long toIncrease) {
331
        increasedCounter++;
1✔
332
        increasedSum += toIncrease;
1✔
333
        long newSize = getDatabaseSize() + toIncrease;
1✔
334
        env.setMapSize(newSize);
1✔
335
    }
1✔
336

337
    @Override
338
    public long getIncreasedCounter() {
339
        return increasedCounter;
1✔
340
    }
341

342
    @Override
343
    public long getIncreasedSum() {
344
        return increasedSum;
1✔
345
    }
346
    
347
    @Override
348
    public void logStats() {
349
        logger.info("##### BEGIN: LMDB stats #####");
1✔
350
        logger.info("... this may take a lot of time ...");
1✔
351
        logger.info("DatabaseSize: " + new ByteConversion().bytesToMib(getDatabaseSize()) + " MiB");
1✔
352
        logger.info("IncreasedCounter: " + getIncreasedCounter());
1✔
353
        logger.info("IncreasedSum: " + new ByteConversion().bytesToMib(getIncreasedSum()) + " MiB");
1✔
354
        logger.info("Stat: " + env.stat());
1✔
355
        // Attention: slow!
356
        long count = count();
1✔
357
        logger.info("LMDB contains " + count + " unique entries.");
1✔
358
        logger.info("##### END: LMDB stats #####");
1✔
359
    }
1✔
360
}
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