From f8fbcdbed308b4d40d6114c0d84b0521e206d262 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 14:35:39 -0700 Subject: [PATCH 1/7] Block: remove explicit `super()` call in constructor --- core/src/main/java/org/bitcoinj/core/Block.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index 54bc937f339..bcc15e84d38 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -224,7 +224,6 @@ private static List readTransactions(ByteBuffer payload) throws Buf */ public Block(long version, Sha256Hash prevHash, Sha256Hash merkleRoot, Instant time, Difficulty difficultyTarget, long nonce, @Nullable List transactions) { - super(); this.version = version; this.prevHash = prevHash; this.merkleRoot = merkleRoot; From 09858b4fb86f6b70fe4167f3a76f39d13a48c20c Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 14:38:42 -0700 Subject: [PATCH 2/7] Block: stop using deprecated `STANDARD_MAX_DIFFICULTY_TARGET` Use `Difficulty.STANDARD_MAX_DIFFICULTY_TARGET.compact()` instead. --- core/src/main/java/org/bitcoinj/core/Block.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index bcc15e84d38..a541d3b71d2 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -265,7 +265,11 @@ public static Block createGenesis(Instant time, long difficultyTarget, long nonc private static List genesisTransactions() { byte[] messageBytes = GENESIS_MESSAGE.getBytes(StandardCharsets.US_ASCII); Script scriptSig = // TODO find out what the pushdata(4) is supposed to mean - new ScriptBuilder().bigNum(STANDARD_MAX_DIFFICULTY_TARGET).bigNum(4).data(messageBytes).build(); + new ScriptBuilder() + .bigNum(Difficulty.STANDARD_MAX_DIFFICULTY_TARGET.compact()) + .bigNum(4) + .data(messageBytes) + .build(); Transaction tx = Transaction.coinbase(scriptSig.program()); tx.addOutput(new TransactionOutput( tx, FIFTY_COINS, ScriptBuilder.createP2PKOutputScript(GENESIS_OUTPUT_PUBKEY).program())); From bbed0da8165ee34c326f81cfe60c0f826a15ce3d Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 14:41:05 -0700 Subject: [PATCH 3/7] Block: make constant `LARGEST_HASH` final --- core/src/main/java/org/bitcoinj/core/Block.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index a541d3b71d2..31688bb2144 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -369,7 +369,7 @@ public Sha256Hash getHash() { * The number that is one greater than the largest representable SHA-256 * hash. */ - private static BigInteger LARGEST_HASH = BigInteger.ONE.shiftLeft(256); + private static final BigInteger LARGEST_HASH = BigInteger.ONE.shiftLeft(256); /** * Returns the work represented by this block.

From 55e5790cad611b1cbd6f3c843f816931d6983f93 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 11:03:20 -0700 Subject: [PATCH 4/7] Block: make `calculateMerkleRoot()`, `calculateWitnessRoot()` calculations static By making these methods pure, static functions they are more flexible and can be used as part of constructor building an immutable block. --- core/src/main/java/org/bitcoinj/core/Block.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index 31688bb2144..1ab73ad9946 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -488,7 +488,7 @@ private void checkSigOps() throws VerificationException { } private void checkMerkleRoot() throws VerificationException { - Sha256Hash calculatedRoot = calculateMerkleRoot(); + Sha256Hash calculatedRoot = calculateMerkleRoot(transactions); if (!calculatedRoot.equals(merkleRoot)) { log.error("Merkle tree did not verify"); throw new VerificationException("Merkle hashes do not match: " + calculatedRoot + " vs " + merkleRoot); @@ -521,17 +521,17 @@ void checkWitnessRoot() throws VerificationException { } } - private Sha256Hash calculateMerkleRoot() { - List tree = buildMerkleTree(false); + private static Sha256Hash calculateMerkleRoot(List transactions) { + List tree = buildMerkleTree(transactions, false); return tree.get(tree.size() - 1); } - private Sha256Hash calculateWitnessRoot() { - List tree = buildMerkleTree(true); + private static Sha256Hash calculateWitnessRoot(List transactions) { + List tree = buildMerkleTree(transactions, true); return tree.get(tree.size() - 1); } - private List buildMerkleTree(boolean useWTxId) { + private static List buildMerkleTree(List transactions, boolean useWTxId) { // The Merkle root is based on a tree of hashes calculated from the transactions: // // root @@ -633,7 +633,7 @@ public Sha256Hash getMerkleRoot() { if (merkleRoot == null) { //TODO check if this is really necessary. unCacheHeader(); - merkleRoot = calculateMerkleRoot(); + merkleRoot = calculateMerkleRoot(transactions); } return merkleRoot; } @@ -651,7 +651,7 @@ void setMerkleRoot(Sha256Hash value) { */ public Sha256Hash getWitnessRoot() { if (witnessRoot == null) - witnessRoot = calculateWitnessRoot(); + witnessRoot = calculateWitnessRoot(transactions); return witnessRoot; } From 3b29e3ee1aef039283a58177e5c4b6d75c2fdf13 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 11:10:53 -0700 Subject: [PATCH 5/7] Block: precalculate `merkleRoot` in Genesis block constructor --- core/src/main/java/org/bitcoinj/core/Block.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index 1ab73ad9946..0d8b58dc9fc 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -209,6 +209,7 @@ private static List readTransactions(ByteBuffer payload) throws Buf this.difficultyTarget = difficultyTarget; this.nonce = nonce; this.prevHash = Sha256Hash.ZERO_HASH; + this.merkleRoot = calculateMerkleRoot(transactions); this.transactions = new ArrayList<>(Objects.requireNonNull(transactions)); } From 7f5c0ed8120a6a9a0aa4264faebd258646de1143 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Sat, 21 Mar 2026 07:33:48 -0700 Subject: [PATCH 6/7] Block: create and use private constructors with pre-calculated hash This eliminates two cases where a `Block` is mutated immediately after construction and also creates block instances that _could_ be treated as immutable. This is a step toward an immutable block type. --- .../main/java/org/bitcoinj/core/Block.java | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index 0d8b58dc9fc..d79f2a53e16 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -28,6 +28,7 @@ import org.bitcoinj.base.internal.InternalUtils; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -151,13 +152,9 @@ public static Block read(ByteBuffer payload) throws BufferUnderflowException, Pr long nonce = ByteUtils.readUint32(payload); payload.reset(); // read again from the mark for the hash Sha256Hash hash = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(Buffers.readBytes(payload, HEADER_SIZE))); - // transactions - List transactions = payload.hasRemaining() ? // otherwise this message is just a header - readTransactions(payload) : - null; - Block block = new Block(version, prevHash, merkleRoot, time, difficultyTarget, nonce, transactions); - block.hash = hash; - return block; + return payload.hasRemaining() + ? new Block(version, prevHash, merkleRoot, time, difficultyTarget, nonce, readTransactions(payload), hash) // full block + : new Block(version, prevHash, merkleRoot, time, difficultyTarget, nonce, hash); // header } /** @@ -236,6 +233,32 @@ public Block(long version, Sha256Hash prevHash, Sha256Hash merkleRoot, Instant t null; } + // block header constructor that takes pre-calculated hash - this block header could be treated as immutable + private Block(long version, Sha256Hash prevHash, Sha256Hash merkleRoot, Instant time, + Difficulty difficultyTarget, long nonce, Sha256Hash hash) { + this.version = version; + this.prevHash = prevHash; + this.merkleRoot = merkleRoot; + this.time = time; + this.difficultyTarget = difficultyTarget; + this.nonce = nonce; + this.transactions = null; + this.hash = hash; + } + + // full block constructor that takes pre-calculated hash - this block could be treated as immutable + private Block(long version, Sha256Hash prevHash, Sha256Hash merkleRoot, Instant time, + Difficulty difficultyTarget, long nonce, @NonNull List transactions, Sha256Hash hash) { + this.version = version; + this.prevHash = prevHash; + this.merkleRoot = merkleRoot; + this.time = time; + this.difficultyTarget = difficultyTarget; + this.nonce = nonce; + this.transactions = new ArrayList<>(Objects.requireNonNull(transactions)); + this.hash = hash; + } + /** @deprecated use {@link #Block(long, Sha256Hash, Sha256Hash, Instant, Difficulty, long, List)} */ @Deprecated public Block(long version, Sha256Hash prevHash, Sha256Hash merkleRoot, Instant time, @@ -391,9 +414,7 @@ public BigInteger getWork() throws VerificationException { * @return new, header-only {@code Block} */ public Block asHeader() { - Block block = new Block(version, prevHash, getMerkleRoot(), time, difficultyTarget, nonce, null); - block.hash = getHash(); - return block; + return new Block(version, prevHash, getMerkleRoot(), time, difficultyTarget, nonce, getHash()); } /** @deprecated use {@link #asHeader()} */ From 69bb7e1c2c4433953a847faeec21395ea359b049 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Fri, 20 Mar 2026 13:46:54 -0700 Subject: [PATCH 7/7] Block: declare some non-nullness guarantees In preparation for being `@NullMarked` we have generally not been annotating non-nullable items, but we make an exception in this case to aid in making null-marked subclasses of `Block` prior to `Block` itself being null-marked. --- core/src/main/java/org/bitcoinj/core/Block.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/Block.java b/core/src/main/java/org/bitcoinj/core/Block.java index d79f2a53e16..6d94e4b6166 100644 --- a/core/src/main/java/org/bitcoinj/core/Block.java +++ b/core/src/main/java/org/bitcoinj/core/Block.java @@ -121,9 +121,12 @@ public enum VerifyFlag { // Fields defined as part of the protocol format. private final long version; + @NonNull private final Sha256Hash prevHash; // previous block private Sha256Hash merkleRoot, witnessRoot; + @NonNull private Instant time; + @NonNull private Difficulty difficultyTarget; // "nBits" private long nonce; @@ -375,6 +378,7 @@ private Sha256Hash calculateHash() { * the block explorer. If you call this on block 1 in the mainnet chain * you will get "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048". */ + @NonNull public String getHashAsString() { return getHash().toString(); } @@ -383,6 +387,7 @@ public String getHashAsString() { * Returns the hash of the block (which for a valid, solved block should be * below the target). Big endian. */ + @NonNull public Sha256Hash getHash() { if (hash == null) hash = calculateHash(); @@ -403,6 +408,7 @@ public Sha256Hash getHash() { * hash values. Then the work of the block will be 20. As the target gets * lower, the amount of work goes up. */ + @NonNull public BigInteger getWork() throws VerificationException { BigInteger target = difficultyTarget.asInteger(); return LARGEST_HASH.divide(target.add(BigInteger.ONE)); @@ -413,6 +419,7 @@ public BigInteger getWork() throws VerificationException { * * @return new, header-only {@code Block} */ + @NonNull public Block asHeader() { return new Block(version, prevHash, getMerkleRoot(), time, difficultyTarget, nonce, getHash()); } @@ -651,6 +658,7 @@ public int hashCode() { /** * Returns the merkle root in big endian form, calculating it from transactions if necessary. */ + @NonNull public Sha256Hash getMerkleRoot() { if (merkleRoot == null) { //TODO check if this is really necessary. @@ -671,6 +679,7 @@ void setMerkleRoot(Sha256Hash value) { /** * Returns the witness root in big endian form, calculating it from transactions if necessary. */ + @NonNull public Sha256Hash getWitnessRoot() { if (witnessRoot == null) witnessRoot = calculateWitnessRoot(transactions); @@ -719,6 +728,7 @@ public long getVersion() { * * @return hash of the previous block */ + @NonNull public Sha256Hash prevHash() { return prevHash; } @@ -732,6 +742,7 @@ public Sha256Hash getPrevBlockHash() { /** * Returns the time at which the block was solved and broadcast, according to the clock of the solving node. */ + @NonNull public Instant time() { return time; } @@ -762,6 +773,7 @@ void setTime(Instant time) { * That number is the result of applying a formula to the underlying difficulty to normalize the minimum to 1. * Calculating the difficulty that way is currently unsupported. */ + @NonNull public Difficulty difficultyTarget() { return difficultyTarget; } @@ -774,7 +786,7 @@ public long getDifficultyTarget() { /** Sets the difficulty target. */ // For testing only - void setDifficultyTarget(Difficulty difficultyTarget) { + void setDifficultyTarget(@NonNull Difficulty difficultyTarget) { unCacheHeader(); this.difficultyTarget = difficultyTarget; this.hash = null;