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

bernardladenthin / BitcoinAddressFinder / #246

08 Apr 2025 06:29AM UTC coverage: 59.356% (-6.0%) from 65.404%
#246

push

bernardladenthin
Add timeout. Extract interruptAfterDelay. Ignore testRoundtrip_configurationsGiven_lmdbCreatedExportedAndRunFindSecretsFile for now.

1050 of 1769 relevant lines covered (59.36%)

0.59 hits per line

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

74.19
/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.bitcoinj.core.LegacyAddress;
24
import org.bitcoinj.core.Coin;
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.AddressTxtLine;
40
import net.ladenthin.bitcoinaddressfinder.ByteBufferUtility;
41
import net.ladenthin.bitcoinaddressfinder.ByteConversion;
42
import net.ladenthin.bitcoinaddressfinder.KeyUtility;
43
import net.ladenthin.bitcoinaddressfinder.SeparatorFormat;
44
import net.ladenthin.bitcoinaddressfinder.configuration.CAddressFileOutputFormat;
45
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationReadOnly;
46
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationWrite;
47
import org.apache.commons.codec.binary.Hex;
48
import org.lmdbjava.BufferProxy;
49
import org.lmdbjava.ByteBufferProxy;
50

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

57
public class LMDBPersistence implements Persistence {
58

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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