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

bernardladenthin / BitcoinAddressFinder / #266

08 Apr 2025 07:49PM UTC coverage: 64.041% (-0.3%) from 64.326%
#266

push

bernardladenthin
Add LMDBPlatformAssume

1138 of 1777 relevant lines covered (64.04%)

0.64 hits per line

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

81.58
/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.PublicKeyBytes;
41
import net.ladenthin.bitcoinaddressfinder.SeparatorFormat;
42
import net.ladenthin.bitcoinaddressfinder.configuration.CAddressFileOutputFormat;
43
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationReadOnly;
44
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationWrite;
45
import org.apache.commons.codec.binary.Hex;
46
import org.bitcoinj.base.Coin;
47
import org.bitcoinj.base.LegacyAddress;
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.network, 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.network, 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();
1✔
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();
1✔
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
            return getCoinFromByteBuffer(byteBuffer);
1✔
180
        }
181
    }
182
    
183
    private Coin getCoinFromByteBuffer(ByteBuffer byteBuffer) {
184
        if (byteBuffer != null) {
1✔
185
            if (byteBuffer.capacity() == 0) {
1✔
186
                return Coin.ZERO;
1✔
187
            } else {
188
                return Coin.valueOf(byteBuffer.getLong());
1✔
189
            }
190
        } else {
191
            return Coin.ZERO;
×
192
        }
193
    }
194

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

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

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

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

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

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

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

320
    @Override
321
    public long getDatabaseSize() {
322
        EnvInfo info = env.info();
1✔
323
        return info.mapSize;
1✔
324
    }
325

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

334
    @Override
335
    public long getIncreasedCounter() {
336
        return increasedCounter;
1✔
337
    }
338

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