Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
eb1ddef
Fiat: enforce non-null field `currencyCode` in constructor
msgilligan Mar 12, 2026
18f4f0c
VersionTally, VersionTallyTest: make nullability explicit
msgilligan Mar 12, 2026
854e325
ExchangeRateTest: remove trivial nullability test
msgilligan Mar 12, 2026
ddf7b87
BtcFormat, BlockFileLoader: annotate nullable fields and methods
msgilligan Mar 12, 2026
709394f
package-info.java: declare nullness expectations for package `o.b.utils`
msgilligan Mar 12, 2026
6d62fc6
BlockStore, FullPrunedBlockStore, MemoryBlockStore, MemoryFullPrunedB…
msgilligan Mar 16, 2026
56dd782
MemoryBlockStore: use `this` rather than reference own instance
msgilligan Mar 16, 2026
d667291
MemoryBlockStore: set `chainHead` directly in the constructor
msgilligan Mar 16, 2026
01e3917
SPVBlockStore: make field `randomAccessFile` final and non-nullable
msgilligan Mar 16, 2026
01e1b5d
SPVBlockStore: handle null fields `buffer`, `fileLock` in `close()`
msgilligan Mar 16, 2026
97834bb
SPVBlockStore: clear block cache in `finally` clause in `close()`
msgilligan Mar 16, 2026
47ce738
MemoryFullPrunedBlockStore: move init of field `network` prior to try…
msgilligan Mar 16, 2026
03dba94
MemoryFullPrunedBlockStore: move init of locals prior to try block
msgilligan Mar 16, 2026
9c81ed9
MemoryFullPrunedBlockStore: replace empty list local with `Collection…
msgilligan Mar 16, 2026
a1af66a
MemoryFullPrunedBlockStore: set fields `chainHead`, `verifiedChainHea…
msgilligan Mar 16, 2026
704f87f
MemoryFullPrunedBlockStore, SPVBlockStore, WalletProtobufSerializerTe…
msgilligan Mar 16, 2026
3272e5e
package-info.java: declare nullness expectations for package `o.b.store`
msgilligan Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion base/src/main/java/org/bitcoinj/base/utils/Fiat.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class Fiat implements Monetary, Comparable<Fiat> {

private Fiat(final String currencyCode, final long value) {
this.value = value;
this.currencyCode = currencyCode;
this.currencyCode = Objects.requireNonNull(currencyCode);
}

public static Fiat valueOf(final String currencyCode, final long value) {
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/org/bitcoinj/store/BlockStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.jspecify.annotations.Nullable;

/**
* An implementor of BlockStore saves StoredBlock objects to disk. Different implementations store them in
Expand All @@ -42,6 +43,7 @@ public interface BlockStore {
* Returns the StoredBlock given a hash. The returned values block.getHash() method will be equal to the
* parameter. If no such block is found, returns null.
*/
@Nullable
StoredBlock get(Sha256Hash hash) throws BlockStoreException;

/**
Expand All @@ -56,7 +58,7 @@ public interface BlockStore {
* Sets the {@link StoredBlock} that represents the top of the chain of greatest total work.
*/
void setChainHead(StoredBlock chainHead) throws BlockStoreException;

/** Closes the store. */
void close() throws BlockStoreException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.bitcoinj.core.StoredUndoableBlock;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.core.UTXOProvider;
import org.jspecify.annotations.Nullable;


/**
Expand Down Expand Up @@ -66,18 +67,21 @@ public interface FullPrunedBlockStore extends BlockStore, UTXOProvider {
* Returns the StoredBlock that was added as a StoredUndoableBlock given a hash. The returned values block.getHash()
* method will be equal to the parameter. If no such block is found, returns null.
*/
@Nullable
StoredBlock getOnceUndoableStoredBlock(Sha256Hash hash) throws BlockStoreException;

/**
* Returns a {@link StoredUndoableBlock} whose block.getHash() method will be equal to the parameter. If no such
* block is found, returns null. Note that this may return null more often than get(Sha256Hash hash) as not all
* {@link StoredBlock}s have a {@link StoredUndoableBlock} copy stored as well.
*/
@Nullable
StoredUndoableBlock getUndoBlock(Sha256Hash hash) throws BlockStoreException;

/**
* Gets a {@link UTXO} with the given hash and index, or null if none is found
*/
@Nullable
UTXO getTransactionOutput(Sha256Hash hash, long index) throws BlockStoreException;

/**
Expand All @@ -103,6 +107,7 @@ public interface FullPrunedBlockStore extends BlockStore, UTXOProvider {
* been fully verified and the point in the chain at which the unspent transaction output set in this
* store represents.
*/
@Nullable
StoredBlock getVerifiedChainHead() throws BlockStoreException;

/**
Expand Down
8 changes: 5 additions & 3 deletions core/src/main/java/org/bitcoinj/store/MemoryBlockStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.VerificationException;
import org.jspecify.annotations.Nullable;

import java.util.LinkedHashMap;
import java.util.Map;
Expand All @@ -28,10 +29,10 @@
* Keeps {@link StoredBlock}s in memory. Used primarily for unit testing.
*/
public class MemoryBlockStore implements BlockStore {
private LinkedHashMap<Sha256Hash, StoredBlock> blockMap = new LinkedHashMap<Sha256Hash, StoredBlock>() {
private @Nullable LinkedHashMap<Sha256Hash, StoredBlock> blockMap = new LinkedHashMap<Sha256Hash, StoredBlock>() {
@Override
protected boolean removeEldestEntry(Map.Entry<Sha256Hash, StoredBlock> eldest) {
return blockMap.size() > 5000;
return this.size() > 5000;
}
};
private StoredBlock chainHead;
Expand All @@ -41,7 +42,7 @@ public MemoryBlockStore(Block genesisBlock) {
Block genesisHeader = genesisBlock.asHeader();
StoredBlock storedGenesis = new StoredBlock(genesisHeader, genesisHeader.getWork(), 0);
put(storedGenesis);
setChainHead(storedGenesis);
chainHead = storedGenesis;
} catch (BlockStoreException | VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
Expand All @@ -55,6 +56,7 @@ public synchronized final void put(StoredBlock block) throws BlockStoreException
}

@Override
@Nullable
public synchronized StoredBlock get(Sha256Hash hash) throws BlockStoreException {
if (blockMap == null) throw new BlockStoreException("MemoryBlockStore is closed");
return blockMap.get(hash);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
Expand Down Expand Up @@ -194,10 +195,10 @@ protected static class StoredBlockAndWasUndoableFlag {
public boolean wasUndoable;
public StoredBlockAndWasUndoableFlag(StoredBlock block, boolean wasUndoable) { this.block = block; this.wasUndoable = wasUndoable; }
}
private TransactionalHashMap<Sha256Hash, StoredBlockAndWasUndoableFlag> blockMap;
private TransactionalFullBlockMap fullBlockMap;
private @Nullable TransactionalHashMap<Sha256Hash, StoredBlockAndWasUndoableFlag> blockMap;
private @Nullable TransactionalFullBlockMap fullBlockMap;
//TODO: Use something more suited to remove-heavy use?
private TransactionalHashMap<TransactionOutPoint, UTXO> transactionOutputMap;
private @Nullable TransactionalHashMap<TransactionOutPoint, UTXO> transactionOutputMap;
private StoredBlock chainHead;
private StoredBlock verifiedChainHead;
private final int fullStoreDepth;
Expand All @@ -209,20 +210,19 @@ protected static class StoredBlockAndWasUndoableFlag {
* @param fullStoreDepth The depth of blocks to keep FullStoredBlocks instead of StoredBlocks
*/
public MemoryFullPrunedBlockStore(NetworkParameters params, int fullStoreDepth) {
network = params.network();
blockMap = new TransactionalHashMap<>();
fullBlockMap = new TransactionalFullBlockMap();
transactionOutputMap = new TransactionalHashMap<>();
this.fullStoreDepth = fullStoreDepth > 0 ? fullStoreDepth : 1;
// Insert the genesis block.
StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().asHeader(), params.getGenesisBlock().getWork(), 0);
// The coinbase in the genesis block is not spendable
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), Collections.emptyList());
try {
StoredBlock storedGenesisHeader = new StoredBlock(params.getGenesisBlock().asHeader(), params.getGenesisBlock().getWork(), 0);
// The coinbase in the genesis block is not spendable
List<Transaction> genesisTransactions = new LinkedList<>();
StoredUndoableBlock storedGenesis = new StoredUndoableBlock(params.getGenesisBlock().getHash(), genesisTransactions);
put(storedGenesisHeader, storedGenesis);
setChainHead(storedGenesisHeader);
setVerifiedChainHead(storedGenesisHeader);
network = params.network();
chainHead = storedGenesisHeader;
verifiedChainHead = storedGenesisHeader;
} catch (BlockStoreException | VerificationException e) {
throw new RuntimeException(e); // Cannot happen.
}
Expand All @@ -238,6 +238,8 @@ public synchronized void put(StoredBlock block) throws BlockStoreException {
@Override
public synchronized final void put(StoredBlock storedBlock, StoredUndoableBlock undoableBlock) throws BlockStoreException {
Objects.requireNonNull(blockMap, "MemoryFullPrunedBlockStore is closed");
Objects.requireNonNull(fullBlockMap);
Objects.requireNonNull(blockMap);
Sha256Hash hash = storedBlock.getHeader().getHash();
fullBlockMap.put(hash, storedBlock.getHeight(), undoableBlock);
blockMap.put(hash, new StoredBlockAndWasUndoableFlag(storedBlock, true));
Expand Down Expand Up @@ -292,6 +294,7 @@ public synchronized final void setVerifiedChainHead(StoredBlock chainHead) throw
setChainHead(chainHead);
// Potential leak here if not all blocks get setChainHead'd
// Though the FullPrunedBlockStore allows for this, the current AbstractBlockChain will not do it.
Objects.requireNonNull(fullBlockMap);
fullBlockMap.removeByHeight(chainHead.getHeight() - fullStoreDepth);
}

Expand Down Expand Up @@ -324,20 +327,29 @@ public synchronized void removeUnspentTransactionOutput(UTXO out) throws BlockSt

@Override
public synchronized void beginDatabaseBatchWrite() throws BlockStoreException {
Objects.requireNonNull(blockMap);
Objects.requireNonNull(fullBlockMap);
Objects.requireNonNull(transactionOutputMap);
blockMap.beginDatabaseBatchWrite();
fullBlockMap.BeginTransaction();
transactionOutputMap.beginDatabaseBatchWrite();
}

@Override
public synchronized void commitDatabaseBatchWrite() throws BlockStoreException {
Objects.requireNonNull(blockMap);
Objects.requireNonNull(fullBlockMap);
Objects.requireNonNull(transactionOutputMap);
blockMap.commitDatabaseBatchWrite();
fullBlockMap.CommitTransaction();
transactionOutputMap.commitDatabaseBatchWrite();
}

@Override
public synchronized void abortDatabaseBatchWrite() throws BlockStoreException {
Objects.requireNonNull(blockMap);
Objects.requireNonNull(fullBlockMap);
Objects.requireNonNull(transactionOutputMap);
blockMap.abortDatabaseBatchWrite();
fullBlockMap.AbortTransaction();
transactionOutputMap.abortDatabaseBatchWrite();
Expand All @@ -359,14 +371,15 @@ public Network network() {
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return getVerifiedChainHead().getHeight();
return Objects.requireNonNull(getVerifiedChainHead()).getHeight();
} catch (BlockStoreException e) {
throw new UTXOProviderException(e);
}
}

@Override
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
Objects.requireNonNull(transactionOutputMap, "MemoryFullPrunedBlockStore is closed");
// This is *NOT* optimal: We go through all the outputs and select the ones we are looking for.
// If someone uses this store for production then they have a lot more to worry about than an inefficient impl :)
List<UTXO> foundOutputs = new ArrayList<>();
Expand Down
36 changes: 26 additions & 10 deletions core/src/main/java/org/bitcoinj/store/SPVBlockStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import org.jspecify.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.Buffer;
Expand Down Expand Up @@ -65,7 +66,7 @@ public class SPVBlockStore implements BlockStore {
// Magic header for the V2 format.
static final byte[] HEADER_MAGIC_V2 = "SPV2".getBytes(StandardCharsets.US_ASCII);

protected volatile MappedByteBuffer buffer;
protected volatile @Nullable MappedByteBuffer buffer;
protected final NetworkParameters params;

// The entire ring-buffer is mmapped and accessing it should be as fast as accessing regular memory once it's
Expand Down Expand Up @@ -95,8 +96,8 @@ protected boolean removeEldestEntry(Map.Entry<Sha256Hash, Object> entry) {
}
};
// Used to stop other applications/processes from opening the store.
protected FileLock fileLock = null;
protected RandomAccessFile randomAccessFile = null;
protected @Nullable FileLock fileLock;
protected final RandomAccessFile randomAccessFile;
private final FileChannel channel;
private int fileLength;

Expand All @@ -123,11 +124,16 @@ public SPVBlockStore(NetworkParameters params, File file, int capacity, boolean
this.params = Objects.requireNonNull(params);
checkArgument(capacity > 0);

try {
boolean exists = file.exists();
boolean exists = file.exists();

try {
// Set up the backing file, empty if it doesn't exist.
randomAccessFile = new RandomAccessFile(file, "rw");
} catch (FileNotFoundException e) {
throw new BlockStoreException(e);
}

try {
channel = randomAccessFile.getChannel();

// Lock the file.
Expand Down Expand Up @@ -192,7 +198,7 @@ public SPVBlockStore(NetworkParameters params, File file, int capacity, boolean
StandardCharsets.US_ASCII));
} catch (Exception e) {
try {
if (randomAccessFile != null) randomAccessFile.close();
randomAccessFile.close();
} catch (IOException e2) {
throw new BlockStoreException(e2);
}
Expand All @@ -201,6 +207,7 @@ public SPVBlockStore(NetworkParameters params, File file, int capacity, boolean
}

private void initNewStore(Block genesisBlock) throws Exception {
Objects.requireNonNull(buffer);
((Buffer) buffer).rewind();
buffer.put(HEADER_MAGIC_V2);
// Insert the genesis block.
Expand All @@ -225,6 +232,7 @@ private void migrateV1toV2() throws BlockStoreException, IOException {

randomAccessFile.setLength(fileLength);
// Map it into memory again because of the length change.
Objects.requireNonNull(buffer);
buffer.force();
buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileLength);

Expand Down Expand Up @@ -327,7 +335,7 @@ public StoredBlock get(Sha256Hash hash) throws BlockStoreException {
} finally { lock.unlock(); }
}

protected StoredBlock lastChainHead = null;
protected @Nullable StoredBlock lastChainHead;

@Override
public StoredBlock getChainHead() throws BlockStoreException {
Expand Down Expand Up @@ -366,14 +374,19 @@ public void setChainHead(StoredBlock chainHead) throws BlockStoreException {

@Override
public void close() throws BlockStoreException {
try {
if (buffer != null) {
buffer.force();
buffer = null; // Allow it to be GCd and the underlying file mapping to go away.
fileLock.release();
}
try {
if (fileLock != null) {
fileLock.release();
}
randomAccessFile.close();
blockCache.clear();
} catch (IOException e) {
throw new BlockStoreException(e);
} finally {
blockCache.clear();
}
}

Expand All @@ -395,6 +408,7 @@ public void close() throws BlockStoreException {

/** Returns the offset from the file start where the latest block should be written (end of prev block). */
int getRingCursor() {
Objects.requireNonNull(buffer);
int c = buffer.getInt(4);
checkState(c >= FILE_PROLOGUE_BYTES, () ->
"integer overflow");
Expand All @@ -403,10 +417,12 @@ int getRingCursor() {

private void setRingCursor(int newCursor) {
checkArgument(newCursor >= 0);
Objects.requireNonNull(buffer);
buffer.putInt(4, newCursor);
}

public void clear() throws Exception {
Objects.requireNonNull(buffer);
lock.lock();
try {
// Clear caches
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/bitcoinj/store/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@
* buffer of headers on disk and is suitable for lightweight user wallets, a store that can calculate a full indexed
* UTXO set (i.e. it can query address balances), and a memory only store useful for unit tests.
*/
@NullMarked
package org.bitcoinj.store;

import org.jspecify.annotations.NullMarked;
4 changes: 3 additions & 1 deletion core/src/main/java/org/bitcoinj/utils/BlockFileLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.bitcoinj.core.MessageSerializer;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.ProtocolException;
import org.jspecify.annotations.Nullable;

import java.io.BufferedInputStream;
import java.io.File;
Expand Down Expand Up @@ -109,7 +110,7 @@ public BlockFileLoader(Network network, List<File> files) {
public class BlockFileIterator implements Iterator<ByteBuffer> {
private final File file;
private final BufferedInputStream currentFileStream;
private ByteBuffer nextBlock = null;
private @Nullable ByteBuffer nextBlock = null;

public BlockFileIterator(File blockFile) throws FileNotFoundException {
this.file = blockFile;
Expand All @@ -124,6 +125,7 @@ public boolean hasNext() {
}

@Override
@Nullable
public ByteBuffer next() throws NoSuchElementException {
if (!hasNext())
throw new NoSuchElementException();
Expand Down
Loading
Loading