From 6c02a7bb9d0602155b66ef4ca3aa15ffbf05d078 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:10:07 -0700 Subject: [PATCH 01/13] fix: improve refreshUnusedKeys for locks --- .../bitcoinj/wallet/CoinJoinExtension.java | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java index b7299807c..8fc21d44a 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java @@ -546,9 +546,20 @@ boolean isKeyUsed(byte[] pubKeyHash) { } public void refreshUnusedKeys() { + // Pre-compute used pubkey hashes outside the lock to avoid O(keys × txes × outputs) + // work while holding unusedKeysLock, which was causing severe lock contention. + Set usedPubKeyHashes = new HashSet<>(); + for (Transaction tx : wallet.getTransactions(true)) { + for (TransactionOutput output : tx.getOutputs()) { + if (ScriptPattern.isP2PKH(output.getScriptPubKey())) { + usedPubKeyHashes.add(ByteString.copyFrom( + ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()))); + } + } + } + List issuedKeys; - Set txes = wallet.getTransactions(true); - + unusedKeysLock.lock(); try { keyChainGroupLock.lock(); @@ -559,28 +570,14 @@ public void refreshUnusedKeys() { keyChainGroupLock.unlock(); } - issuedKeys.forEach(key -> { + for (IDeterministicKey key : issuedKeys) { unusedKeys.put(KeyId.fromBytes(key.getPubKeyHash()), (DeterministicKey) key); - keyUsage.put(key, false); - }); - - Stream usedKeys = issuedKeys.stream().filter(key -> { - boolean found = txes.stream().anyMatch(tx -> - tx.getOutputs().stream().anyMatch(output -> { - if (ScriptPattern.isP2PKH(output.getScriptPubKey())) { - byte[] publicKeyHash = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()); - return Arrays.equals(publicKeyHash, key.getPubKeyHash()); - } else return false; - }) - ); - if (found) { - keyUsage.put(key, true); - } - return found; - } - ); - - usedKeys.forEach(key -> unusedKeys.remove(KeyId.fromBytes(key.getPubKeyHash()))); + boolean used = usedPubKeyHashes.contains(ByteString.copyFrom(key.getPubKeyHash())); + keyUsage.put(key, used); + if (used) { + unusedKeys.remove(KeyId.fromBytes(key.getPubKeyHash())); + } + } unusedKeys.forEach((keyId, key) -> log.info(COINJOIN_EXTRA, "unused key: {}", key)); keyUsage.forEach((key, used) -> { From 5cccfdef54e7ab4cf57f8b0746b76d67c438a687 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:12:21 -0700 Subject: [PATCH 02/13] fix: improve getMixingProgress algorithm accuracy --- .../bitcoinj/wallet/CoinJoinExtension.java | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java index 8fc21d44a..ce692d091 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.AtomicDouble; import com.google.protobuf.ByteString; import com.google.protobuf.CodedOutputStream; import net.jcip.annotations.GuardedBy; @@ -54,6 +55,7 @@ import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; @@ -730,7 +732,9 @@ public String getKeyUsageReport() { } } - public double getMixingProgress() { + // this is the old algorithm + @Deprecated + public double getMixingProgress2() { double requiredRounds = rounds + 0.875; // 1 x 50% + 1 x 50%^2 + 1 x 50%^3 AtomicInteger totalInputs = new AtomicInteger(); AtomicInteger totalRounds = new AtomicInteger(); @@ -760,7 +764,52 @@ public double getMixingProgress() { }); }); double progress = totalInputs.get() != 0 ? totalRounds.get() / (requiredRounds * totalInputs.get()) : 0.0; - log.info("getMixingProgress: {} = {} / ({} * {})", progress, totalRounds.get(), requiredRounds, totalInputs.get()); + log.info("getMixingProgress2: {} = {} / ({} * {})", progress, totalRounds.get(), requiredRounds, totalInputs.get()); + return Math.max(0.0, Math.min(progress, 1.0)); + } + + public double getMixingProgress() { + double requiredRounds = rounds + 0.875; // 1 x 50% + 1 x 50%^2 + 1 x 50%^3 + AtomicInteger totalInputs = new AtomicInteger(); + AtomicDouble totalMixed = new AtomicDouble(); + getOutputs().forEach((denom, outputs) -> { + outputs.forEach(output -> { + // do not count mixing collateral for fees + if (denom >= 0) { + // getOutputs has a bug where non-denominated items are marked as denominated + TransactionOutPoint outPoint = new TransactionOutPoint(output.getParams(), output.getIndex(), output.getParentTransactionHash()); + int roundsMixed = ((WalletEx) wallet).getRealOutpointCoinJoinRounds(outPoint); + + if (roundsMixed >= rounds) { + if (wallet.isFullyMixed(output)) { + totalMixed.addAndGet(1.00); + } else { + totalMixed.addAndGet(((double)roundsMixed) / (roundsMixed + 1)); + } + totalInputs.addAndGet(1); + } else { + if (rounds >= 0) { + totalInputs.addAndGet(1); + double percentMixedForInput = ((double) roundsMixed) / requiredRounds; + totalMixed.addAndGet(percentMixedForInput); + } + } + } else if (denom == -2) { + // estimate what the denominations would be: use greedy algorithm + AtomicInteger unmixedInputs = new AtomicInteger(0); + AtomicReference outputValue = new AtomicReference<>(output.getValue().subtract(CoinJoin.getCollateralAmount())); + CoinJoinClientOptions.getDenominations().forEach(coin -> { + while (outputValue.get().subtract(coin).isGreaterThan(Coin.ZERO)) { + unmixedInputs.getAndIncrement(); + outputValue.set(outputValue.get().subtract(coin)); + } + }); + totalInputs.set(totalInputs.get() + unmixedInputs.get()); + } + }); + }); + double progress = totalInputs.get() != 0 ? totalMixed.get() / totalInputs.get() : 0.0; + log.info("getMixingProgress: {} = {} / {}", progress, totalMixed.get(), totalInputs.get()); return Math.max(0.0, Math.min(progress, 1.0)); } From 7a8fc0ee686c41c22a851446ef593aa4fe27e187 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:13:36 -0700 Subject: [PATCH 03/13] fix: add test for improved getMixingProgress --- .../java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java index 6aea2120c..9f15c93a3 100644 --- a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java @@ -93,8 +93,12 @@ public void balanceAndMixingProgressTest() { info("getBalance(COINJOIN): {}", watch1); Stopwatch watch2 = Stopwatch.createStarted(); - assertEquals(1.00, wallet.getCoinJoin().getMixingProgress(), 0.001); + assertEquals(1.00, wallet.getCoinJoin().getMixingProgress2(), 0.001); info("getMixingProgress: {}", watch2); + + Stopwatch watch3 = Stopwatch.createStarted(); + assertEquals(0.9864790925660492, wallet.getCoinJoin().getMixingProgress(), 0.001); + info("getMixingProgress2: {}", watch3); } @Test From b5d24f7478344cd93576fddd4943bf2948333f0f Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:14:06 -0700 Subject: [PATCH 04/13] fix: PeerGroup: only start downloading from new peer if running --- core/src/main/java/org/bitcoinj/core/PeerGroup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/core/PeerGroup.java b/core/src/main/java/org/bitcoinj/core/PeerGroup.java index 700d42f4d..9b815f816 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerGroup.java +++ b/core/src/main/java/org/bitcoinj/core/PeerGroup.java @@ -2044,7 +2044,7 @@ protected void handlePeerDeath(final Peer peer, @Nullable Throwable exception) { final Peer newDownloadPeer = selectDownloadPeer(peers); if (newDownloadPeer != null) { setDownloadPeer(newDownloadPeer); - if (downloadListener != null) { + if (downloadListener != null && vRunning) { startBlockChainDownloadFromPeer(newDownloadPeer); } } From 38be8af825f7b528c849722ddb5ecf6c21401828 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:14:56 -0700 Subject: [PATCH 05/13] fix: improve shutdown/close operations * don't remove peer listeners, that will be handled by PeerGroup --- .../coinjoin/utils/CoinJoinManager.java | 14 +++--- .../org/bitcoinj/core/AbstractManager.java | 3 ++ .../org/bitcoinj/core/DualBlockChain.java | 48 +++++++++++++++++++ .../org/bitcoinj/core/MasternodeSync.java | 8 ++-- .../java/org/bitcoinj/core/SporkManager.java | 19 ++++++-- .../evolution/AbstractQuorumState.java | 14 +++--- .../evolution/QuorumRotationState.java | 13 +++-- .../SimplifiedMasternodeListManager.java | 7 +-- .../governance/GovernanceManager.java | 8 ++-- .../java/org/bitcoinj/manager/DashSystem.java | 11 ++++- .../bitcoinj/quorums/ChainLocksHandler.java | 10 ++-- .../bitcoinj/quorums/InstantSendManager.java | 6 ++- 12 files changed, 122 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java index d65de3e6b..59b56c537 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -290,13 +290,11 @@ public void close() { blockChain.removeNewBestBlockListener(newBestBlockListener); blockChain.removeTransactionReceivedListener(transactionReceivedInBlockListener); } - if (peerGroup != null) { - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } - if (masternodeGroup != null) { - masternodeGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } - // Ensure executor is shut down + // removePreMessageReceivedEventListener skipped on both peerGroup and masternodeGroup: + // handlePeerDeath() removes it per-peer on disconnect. Calling it here acquires the + // PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + + // Shut down the executor before nulling fields — queued tasks may still reference them. ExecutorService execToStop = null; lock.lock(); try { @@ -310,6 +308,8 @@ public void close() { if (execToStop != null) { execToStop.shutdown(); } + blockChain = null; + peerGroup = null; } public boolean isMasternodeOrDisconnectRequested(MasternodeAddress address) { diff --git a/core/src/main/java/org/bitcoinj/core/AbstractManager.java b/core/src/main/java/org/bitcoinj/core/AbstractManager.java index dfd37bde7..47b6f642e 100644 --- a/core/src/main/java/org/bitcoinj/core/AbstractManager.java +++ b/core/src/main/java/org/bitcoinj/core/AbstractManager.java @@ -308,6 +308,9 @@ public void setFilename(String filename) { autosaveToFile(new File(filename), DELAY_TIME, TimeUnit.MILLISECONDS, null); } + /** + * Typically called when DashSystem is shutting down. + */ public void close() { if (vFileManager != null) { shutdownAutosaveAndWait(); diff --git a/core/src/main/java/org/bitcoinj/core/DualBlockChain.java b/core/src/main/java/org/bitcoinj/core/DualBlockChain.java index bec3ede4b..5ed354c6b 100644 --- a/core/src/main/java/org/bitcoinj/core/DualBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/DualBlockChain.java @@ -16,7 +16,9 @@ package org.bitcoinj.core; +import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.Threading; import javax.annotation.Nullable; @@ -105,4 +107,50 @@ public StoredBlock getBlock(int height) { public StoredBlock getChainHead() { return getLongestChain().chainHead; } + + PeerGroup peerGroup; + MyDownloadProgressTracker downloadProgressTracker; + + private static class MyDownloadProgressTracker extends DownloadProgressTracker { + volatile boolean headerDownloadCompleted = false; + volatile boolean blockDownloadCompleted = false; + MyDownloadProgressTracker(boolean preprocessingBeforeBlocks) { + super(preprocessingBeforeBlocks); + } + + @Override + public void doneHeaderDownload() { + super.doneHeaderDownload(); + headerDownloadCompleted = true; + } + + @Override + protected void doneDownload() { + super.doneDownload(); + blockDownloadCompleted = true; + } + } + + public void setPeerGroup(PeerGroup peerGroup, MasternodeSync masternodeSync) { + if (peerGroup != null) { + this.peerGroup = peerGroup; + downloadProgressTracker = new MyDownloadProgressTracker(masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_BLOCKS_AFTER_PREPROCESSING)); + peerGroup.addHeadersDownloadedEventListener(Threading.USER_THREAD, downloadProgressTracker); + peerGroup.addHeadersDownloadStartedEventListener(Threading.USER_THREAD, downloadProgressTracker); + peerGroup.addBlocksDownloadedEventListener(Threading.USER_THREAD, downloadProgressTracker); + peerGroup.addChainDownloadStartedEventListener(Threading.USER_THREAD, downloadProgressTracker); + peerGroup.addMasternodeListDownloadListener(Threading.USER_THREAD, downloadProgressTracker); + } + } + + public boolean isInitialHeaderSyncComplete() { + return downloadProgressTracker != null && (downloadProgressTracker.headerDownloadCompleted || downloadProgressTracker.blockDownloadCompleted); + } + + public void close() { + if (peerGroup != null) { + // no need to check remove event listeners from peergroup + peerGroup = null; + } + } } diff --git a/core/src/main/java/org/bitcoinj/core/MasternodeSync.java b/core/src/main/java/org/bitcoinj/core/MasternodeSync.java index 48f003e16..b7cd89eaf 100644 --- a/core/src/main/java/org/bitcoinj/core/MasternodeSync.java +++ b/core/src/main/java/org/bitcoinj/core/MasternodeSync.java @@ -123,9 +123,11 @@ public void setBlockChain(AbstractBlockChain blockChain, PeerGroup peerGroup, Ne } public void close() { - if (peerGroup != null) { - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } + // No per-peer listener removal needed: handlePeerDeath() cleans up preMessageReceivedEventListener + // from each peer when it disconnects. Calling removePreMessageReceivedEventListener() here would + // acquire the PeerGroup lock via getConnectedPeers(), which can deadlock during shutdown. + peerGroup = null; + blockChain = null; } public MasternodeSync(Context context, boolean isLiteMode, boolean allowInstantSendInLiteMode) { diff --git a/core/src/main/java/org/bitcoinj/core/SporkManager.java b/core/src/main/java/org/bitcoinj/core/SporkManager.java index dbf2c1094..162f0ab7e 100644 --- a/core/src/main/java/org/bitcoinj/core/SporkManager.java +++ b/core/src/main/java/org/bitcoinj/core/SporkManager.java @@ -72,6 +72,7 @@ private static void makeSporkDefinition(SporkId sporkId, long defaultValue) { @GuardedBy("lock") private final HashMap mapSporksCachedValues; @GuardedBy("lock") private final HashSet setSporkPubKeyIds = new HashSet<>(); + private PeerGroup peerGroup; private AbstractBlockChain blockChain; private MasternodeSync masternodeSync; private final Context context; @@ -91,6 +92,7 @@ public SporkManager(Context context) public void setBlockChain(AbstractBlockChain blockChain, @Nullable PeerGroup peerGroup, MasternodeSync masternodeSync) { this.blockChain = blockChain; this.masternodeSync = masternodeSync; + this.peerGroup = peerGroup; if (peerGroup != null) { peerGroup.addConnectedEventListener(peerConnectedEventListener); peerGroup.addPreMessageReceivedEventListener(SAME_THREAD, preMessageReceivedEventListener); @@ -102,11 +104,16 @@ public void clear() { mapSporksByHash.clear(); } - public void close(PeerGroup peerGroup) { - if (peerGroup != null) { - peerGroup.removeConnectedEventListener(peerConnectedEventListener); - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } + public void close() { + // No per-peer listener removal needed here: + // - preMessageReceivedEventListener is removed from each peer by PeerGroup.handlePeerDeath() + // - peerConnectedEventListener left on a disconnecting peer is harmless (it will never fire) + // Calling removeConnectedEventListener/removePreMessageReceivedEventListener would iterate + // getConnectedPeers() under the PeerGroup lock, which can deadlock during shutdown when + // another PeerGroup thread holds that lock while blocked on SPVBlockStore I/O. + peerGroup = null; + blockChain = null; + masternodeSync = null; } void processSpork(Peer from, SporkMessage spork) { @@ -114,6 +121,8 @@ void processSpork(Peer from, SporkMessage spork) { // return; //disable all darksend/masternode related functionality // } + if (blockChain == null) return; // closed during shutdown + Sha256Hash hash = spork.getHash(); String logMessage = String.format("SPORK -- hash: %s id: %d (%s) value: %10d bestHeight: %d peer=%s:%d", hash, spork.getSporkId().value, String.format("%1$35s", spork.getSporkId().name()), diff --git a/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java b/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java index 6c7c83934..6ab4a8d0a 100644 --- a/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java +++ b/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java @@ -554,12 +554,10 @@ public void removeEventListeners(AbstractBlockChain blockChain, PeerGroup peerGr blockChain.removeNewBestBlockListener(newBestBlockListener); blockChain.removeReorganizeListener(reorganizeListener); } - if (peerGroup != null) { - peerGroup.removeConnectedEventListener(peerConnectedEventListener); - peerGroup.removeChainDownloadStartedEventListener(chainDownloadStartedEventListener); - peerGroup.removeHeadersDownloadStartedEventListener(headersDownloadStartedEventListener); - peerGroup.removeDisconnectedEventListener(peerDisconnectedEventListener); - } + // All peerGroup.remove*EventListener calls skipped: handlePeerDeath() removes per-peer listeners + // (ChainDownloadStarted, Disconnected) when peers disconnect, and Connected/HeadersDownloadStarted + // listeners left on dying peers are benign (those events never fire on disconnecting peers). + // Calling these methods acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. } public final NewBestBlockListener newBestBlockListener = new NewBestBlockListener() { @@ -597,6 +595,7 @@ public void notifyNewBestBlock(StoredBlock block) throws VerificationException { public final PeerConnectedEventListener peerConnectedEventListener = new PeerConnectedEventListener() { @Override public void onPeerConnected(Peer peer, int peerCount) { + if (peerGroup == null) return; // closed during shutdown downloadPeer = peerGroup.getDownloadPeer(); log.info("peer connected and setting download peer to {} with onPeerConnected", downloadPeer); } @@ -605,6 +604,7 @@ public void onPeerConnected(Peer peer, int peerCount) { final PeerDisconnectedEventListener peerDisconnectedEventListener = new PeerDisconnectedEventListener() { @Override public void onPeerDisconnected(Peer peer, int peerCount) { + if (peerGroup == null) return; // closed during shutdown if (downloadPeer == peer) { downloadPeer = peerGroup.getDownloadPeer(); log.info("setting download peer to {} with onPeerDisconnected, previously was {}", downloadPeer, peer); @@ -897,5 +897,7 @@ public void close() { retryFuture.cancel(true); retryFuture = null; } + peerGroup = null; + blockChain = null; } } diff --git a/core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java b/core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java index bee949fac..f642772ad 100644 --- a/core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java +++ b/core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java @@ -445,13 +445,20 @@ public boolean isSynced() { if (blockChain != null && !params.isDIP0024Active(blockChain.getBestChainHeight())) return true; - if(mnListAtH.getHeight() == -1) + if (mnListAtH.getHeight() == -1) return false; - if (peerGroup == null) + if (blockChain == null) return false; - int mostCommonHeight = peerGroup.getMostCommonHeight(); + if (!blockChain.isInitialHeaderSyncComplete()) { + return false; + } + + // Use local chain height instead of peerGroup.getMostCommonHeight() to avoid + // acquiring the PeerGroup lock, which can deadlock during shutdown or when the + // PeerGroup thread holds it while blocked on SPVBlockStore I/O. + int mostCommonHeight = blockChain.getBestChainHeight(); // determine when the last QR height was LLMQParameters llmqParameters = params.getLlmqs().get(llmqType); diff --git a/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java b/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java index ef57f00dd..f8eeccc73 100644 --- a/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java +++ b/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java @@ -443,15 +443,16 @@ public void close() { quorumState.close(); quorumRotationState.close(); - if (peerGroup != null) { - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } + // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. + // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. threadPool.shutdown(); // Don't wait at all - let it die naturally to avoid blocking if (!threadPool.isTerminated()) { log.info("ThreadPool shutdown initiated, not waiting"); threadPool.shutdownNow(); // Send interrupt signal but don't wait } + peerGroup = null; + blockChain = null; saveNow(); // Always save, regardless of thread pool state super.close(); diff --git a/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java b/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java index 0fd2f9218..d3bb7c799 100644 --- a/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java +++ b/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java @@ -1715,9 +1715,9 @@ public void run() { @Override public void close() { - if (peerGroup != null) { - peerGroup.removeGetDataEventListener(getDataEventListener); - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } + // All peerGroup.remove*EventListener calls skipped: handlePeerDeath() removes per-peer listeners + // when peers disconnect. Calling these methods acquires the PeerGroup lock via getConnectedPeers(), + // risking shutdown deadlock. + peerGroup = null; } } diff --git a/core/src/main/java/org/bitcoinj/manager/DashSystem.java b/core/src/main/java/org/bitcoinj/manager/DashSystem.java index 3207d77e5..a75ee28f8 100644 --- a/core/src/main/java/org/bitcoinj/manager/DashSystem.java +++ b/core/src/main/java/org/bitcoinj/manager/DashSystem.java @@ -58,6 +58,7 @@ public class DashSystem { public AbstractBlockChain blockChain; @Nullable public AbstractBlockChain headerChain; + DualBlockChain dualBlockChain; public SporkManager sporkManager; public MasternodePayments masternodePayments; public MasternodeSync masternodeSync; @@ -251,10 +252,13 @@ private void stopLLMQThread() { } } + /** + * close down DashSystem and free resources, typically called after the PeerGroup is shutdown + */ public void close() { if (initializedObjects) { log.info("Closing network spork configuration manager"); - sporkManager.close(peerGroup); + sporkManager.close(); log.info("Closing masternode synchronization state"); masternodeSync.close(); log.info("Closing masternode list manager (includes thread pool shutdown)"); @@ -285,6 +289,8 @@ public void close() { log.info("Closing header chain"); headerChain.close(); } + log.info("closing dual blockchain"); + dualBlockChain.close(); log.info("Clearing peer group reference"); peerGroup = null; } @@ -295,13 +301,14 @@ public void setPeerGroupAndBlockChain(PeerGroup peerGroup, AbstractBlockChain bl this.peerGroup = peerGroup; this.blockChain = blockChain; this.headerChain = headerChain; - DualBlockChain dualBlockChain = new DualBlockChain(headerChain, blockChain); + dualBlockChain = new DualBlockChain(headerChain, blockChain); hashStore = new HashStore(blockChain.getBlockStore()); blockChain.addNewBestBlockListener(newBestBlockListener); handleActivations(blockChain.getChainHead()); if (initializedObjects) { sporkManager.setBlockChain(blockChain, peerGroup, masternodeSync); masternodeSync.setBlockChain(blockChain, peerGroup, netFullfilledRequestManager, governanceManager); + dualBlockChain.setPeerGroup(peerGroup, masternodeSync); masternodeListManager.setBlockChain( dualBlockChain, peerGroup, diff --git a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java index d9472483a..2be417594 100644 --- a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java +++ b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java @@ -130,9 +130,8 @@ public void setBlockChain(PeerGroup peerGroup, AbstractBlockChain blockChain, Ab public void close() { blockChain = null; headerChain = null; - if (peerGroup != null) { - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); - } + // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. + // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. peerGroup = null; super.close(); } @@ -203,8 +202,9 @@ public void processChainLockSignature(Peer peer, ChainLockSignature clsig) processNewChainLock(peer, clsig, hash); } - void processNewChainLock(Peer from, ChainLockSignature clsig, Sha256Hash hash) - { + void processNewChainLock(Peer from, ChainLockSignature clsig, Sha256Hash hash) { + if (blockChain == null) return; // closed during shutdown + lock.lock(); try { if (seenChainLocks.put(hash, Utils.currentTimeMillis()) != null) { diff --git a/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java b/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java index 527393534..ffe3a3f2a 100644 --- a/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java +++ b/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java @@ -127,11 +127,15 @@ public void close(PeerGroup peerGroup) { } if (peerGroup != null) { peerGroup.removeOnTransactionBroadcastListener(this.transactionBroadcastListener); - peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. + // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. } if (chainLocksHandler != null) { chainLocksHandler.removeChainLockListener(this.chainLockListener); } + blockChain = null; + peerGroup = null; + chainLocksHandler = null; wallets.forEach(wallet -> wallet.removeCoinsSentEventListener(coinsSentEventListener)); try { if (!scheduledExecutorService.awaitTermination(3000, TimeUnit.MILLISECONDS)) { From 28992b77c63571efa70cbb9258a3572d4fce6367 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 29 Apr 2026 07:15:19 -0700 Subject: [PATCH 06/13] feat (examples): Add DumpMasternodeList --- .../bitcoinj/examples/DumpMasternodeList.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java diff --git a/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java b/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java new file mode 100644 index 000000000..68105c63b --- /dev/null +++ b/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024 Dash Core Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.examples; + +import com.google.common.util.concurrent.SettableFuture; +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Peer; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.evolution.GetSimplifiedMasternodeListDiff; +import org.bitcoinj.evolution.Masternode; +import org.bitcoinj.evolution.MasternodeType; +import org.bitcoinj.evolution.SimplifiedMasternodeListDiff; +import org.bitcoinj.net.discovery.DnsDiscovery; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.MemoryBlockStore; +import org.bitcoinj.utils.BriefLogFormatter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.bitcoinj.utils.Threading.SAME_THREAD; + +public class DumpMasternodeList { + public static void main(String[] args) throws BlockStoreException, ExecutionException, InterruptedException { + if (args.length < 2) { + System.out.println("DumpMasternodeList network blockHash"); + System.out.println(" one or more arguments are missing!"); + return; + } + BriefLogFormatter.init(); + String network = args[0]; + String blockHash = args[1]; + + NetworkParameters params; + switch (network) { + case "testnet": + params = TestNet3Params.get(); + break; + default: + params = MainNetParams.get(); + break; + } + Context context = Context.getOrCreate(params); + BlockStore blockStore = new MemoryBlockStore(params); + BlockChain chain = new BlockChain(params, blockStore); + PeerGroup peerGroup = new PeerGroup(params, chain); + peerGroup.addPeerDiscovery(new DnsDiscovery(params)); + peerGroup.setUseLocalhostPeerWhenPossible(false); + + peerGroup.start(); + peerGroup.waitForPeers(10).get(); + SettableFuture mnlistdiffReceivedFuture = SettableFuture.create(); + peerGroup.addPreMessageReceivedEventListener(SAME_THREAD, (peer1, m) -> { + try { + if (m instanceof SimplifiedMasternodeListDiff) { + System.out.println("Received mnlistdiff..."); + File dumpFile = new File(params.getNetworkName() + "-mnlist.dat"); + OutputStream stream = new FileOutputStream(dumpFile); + stream.write(m.bitcoinSerialize()); + stream.close(); + mnlistdiffReceivedFuture.set(true); + SimplifiedMasternodeListDiff diff = (SimplifiedMasternodeListDiff)m; + AtomicInteger countLegacy = new AtomicInteger(); + AtomicInteger countEnabled = new AtomicInteger(); + ArrayList evoNodes = new ArrayList<>(); + diff.getMnList().forEach(entry -> { + if (entry.isValid()) { + countLegacy.addAndGet((short) (entry.getVersion() == 1 ? 1 : 0)); + countEnabled.addAndGet(1); + if (MasternodeType.getMasternodeType(entry.getType()) == MasternodeType.HIGHPERFORMANCE) { + evoNodes.add(entry); + } + //System.out.println(countEnabled + " " + entry.getService().getAddr().getHostAddress()); + } + }); + System.out.printf("Total: %d, Legacy: %d\n", countEnabled.get(), countLegacy.get()); + System.out.println("HP Masternode List"); + evoNodes.forEach(entry -> System.out.println("\"" + entry.getService().getAddr().getHostAddress() + "\",")); + return null; + } + } catch (FileNotFoundException e) { + System.out.println("cannot find the file to write to"); + throw new RuntimeException(e); + } catch (IOException e) { + System.out.println("IO Error"); + e.printStackTrace(); + throw new RuntimeException(e); + } + + return m; + }); + Peer peer = peerGroup.getDownloadPeer(); + + peer.sendMessage(new GetSimplifiedMasternodeListDiff(params.getGenesisBlock().getHash(), Sha256Hash.wrap(blockHash))); + + + mnlistdiffReceivedFuture.get(); + peerGroup.stopAsync(); + } +} From 09325a94e0a8434ba28faaa8185b5c8e2c80cff3 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 5 May 2026 07:41:18 -0700 Subject: [PATCH 07/13] fix: restore removal of peergroup listeners --- .../main/java/org/bitcoinj/core/DualBlockChain.java | 12 +++++++++--- .../main/java/org/bitcoinj/core/MasternodeSync.java | 6 +++--- .../main/java/org/bitcoinj/core/SporkManager.java | 10 ++++------ .../org/bitcoinj/evolution/AbstractQuorumState.java | 10 ++++++---- .../evolution/SimplifiedMasternodeListManager.java | 8 ++++---- .../org/bitcoinj/governance/GovernanceManager.java | 7 ++++--- .../java/org/bitcoinj/quorums/ChainLocksHandler.java | 5 +++-- .../org/bitcoinj/quorums/InstantSendManager.java | 3 +-- .../java/org/bitcoinj/wallet/CoinJoinExtension.java | 2 +- .../org/bitcoinj/wallet/LargeCoinJoinWalletTest.java | 4 ++-- 10 files changed, 37 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/DualBlockChain.java b/core/src/main/java/org/bitcoinj/core/DualBlockChain.java index 5ed354c6b..3b50bd863 100644 --- a/core/src/main/java/org/bitcoinj/core/DualBlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/DualBlockChain.java @@ -132,6 +132,7 @@ protected void doneDownload() { } public void setPeerGroup(PeerGroup peerGroup, MasternodeSync masternodeSync) { + close(); if (peerGroup != null) { this.peerGroup = peerGroup; downloadProgressTracker = new MyDownloadProgressTracker(masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_BLOCKS_AFTER_PREPROCESSING)); @@ -148,9 +149,14 @@ public boolean isInitialHeaderSyncComplete() { } public void close() { - if (peerGroup != null) { - // no need to check remove event listeners from peergroup - peerGroup = null; + if (peerGroup != null && downloadProgressTracker != null) { + peerGroup.removeHeadersDownloadedEventListener(downloadProgressTracker); + peerGroup.removeHeadersDownloadStartedEventListener(downloadProgressTracker); + peerGroup.removeBlocksDownloadedEventListener(downloadProgressTracker); + peerGroup.removeChainDownloadStartedEventListener(downloadProgressTracker); + peerGroup.removeMasternodeListDownloadedListener(downloadProgressTracker); } + peerGroup = null; + downloadProgressTracker = null; } } diff --git a/core/src/main/java/org/bitcoinj/core/MasternodeSync.java b/core/src/main/java/org/bitcoinj/core/MasternodeSync.java index b7cd89eaf..e24390243 100644 --- a/core/src/main/java/org/bitcoinj/core/MasternodeSync.java +++ b/core/src/main/java/org/bitcoinj/core/MasternodeSync.java @@ -123,9 +123,9 @@ public void setBlockChain(AbstractBlockChain blockChain, PeerGroup peerGroup, Ne } public void close() { - // No per-peer listener removal needed: handlePeerDeath() cleans up preMessageReceivedEventListener - // from each peer when it disconnects. Calling removePreMessageReceivedEventListener() here would - // acquire the PeerGroup lock via getConnectedPeers(), which can deadlock during shutdown. + if (peerGroup != null) { + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } peerGroup = null; blockChain = null; } diff --git a/core/src/main/java/org/bitcoinj/core/SporkManager.java b/core/src/main/java/org/bitcoinj/core/SporkManager.java index 162f0ab7e..eaab27843 100644 --- a/core/src/main/java/org/bitcoinj/core/SporkManager.java +++ b/core/src/main/java/org/bitcoinj/core/SporkManager.java @@ -105,12 +105,10 @@ public void clear() { } public void close() { - // No per-peer listener removal needed here: - // - preMessageReceivedEventListener is removed from each peer by PeerGroup.handlePeerDeath() - // - peerConnectedEventListener left on a disconnecting peer is harmless (it will never fire) - // Calling removeConnectedEventListener/removePreMessageReceivedEventListener would iterate - // getConnectedPeers() under the PeerGroup lock, which can deadlock during shutdown when - // another PeerGroup thread holds that lock while blocked on SPVBlockStore I/O. + if (peerGroup != null) { + peerGroup.removeConnectedEventListener(peerConnectedEventListener); + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } peerGroup = null; blockChain = null; masternodeSync = null; diff --git a/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java b/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java index 6ab4a8d0a..08091bcd7 100644 --- a/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java +++ b/core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java @@ -554,10 +554,12 @@ public void removeEventListeners(AbstractBlockChain blockChain, PeerGroup peerGr blockChain.removeNewBestBlockListener(newBestBlockListener); blockChain.removeReorganizeListener(reorganizeListener); } - // All peerGroup.remove*EventListener calls skipped: handlePeerDeath() removes per-peer listeners - // (ChainDownloadStarted, Disconnected) when peers disconnect, and Connected/HeadersDownloadStarted - // listeners left on dying peers are benign (those events never fire on disconnecting peers). - // Calling these methods acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + if (peerGroup != null) { + peerGroup.removeConnectedEventListener(peerConnectedEventListener); + peerGroup.removeChainDownloadStartedEventListener(chainDownloadStartedEventListener); + peerGroup.removeHeadersDownloadStartedEventListener(headersDownloadStartedEventListener); + peerGroup.removeDisconnectedEventListener(peerDisconnectedEventListener); + } } public final NewBestBlockListener newBestBlockListener = new NewBestBlockListener() { diff --git a/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java b/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java index f8eeccc73..7707929ff 100644 --- a/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java +++ b/core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java @@ -443,19 +443,19 @@ public void close() { quorumState.close(); quorumRotationState.close(); - // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. - // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + if (peerGroup != null) { + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } threadPool.shutdown(); // Don't wait at all - let it die naturally to avoid blocking if (!threadPool.isTerminated()) { log.info("ThreadPool shutdown initiated, not waiting"); threadPool.shutdownNow(); // Send interrupt signal but don't wait } + saveNow(); // Always save, regardless of thread pool state peerGroup = null; blockChain = null; - saveNow(); // Always save, regardless of thread pool state super.close(); - } } diff --git a/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java b/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java index d3bb7c799..a8c6b03d1 100644 --- a/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java +++ b/core/src/main/java/org/bitcoinj/governance/GovernanceManager.java @@ -1715,9 +1715,10 @@ public void run() { @Override public void close() { - // All peerGroup.remove*EventListener calls skipped: handlePeerDeath() removes per-peer listeners - // when peers disconnect. Calling these methods acquires the PeerGroup lock via getConnectedPeers(), - // risking shutdown deadlock. + if (peerGroup != null) { + peerGroup.removeGetDataEventListener(getDataEventListener); + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } peerGroup = null; } } diff --git a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java index 2be417594..0e0535f91 100644 --- a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java +++ b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java @@ -130,8 +130,9 @@ public void setBlockChain(PeerGroup peerGroup, AbstractBlockChain blockChain, Ab public void close() { blockChain = null; headerChain = null; - // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. - // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + if (peerGroup != null) { + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } peerGroup = null; super.close(); } diff --git a/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java b/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java index ffe3a3f2a..6c2ea9f5e 100644 --- a/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java +++ b/core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java @@ -127,8 +127,7 @@ public void close(PeerGroup peerGroup) { } if (peerGroup != null) { peerGroup.removeOnTransactionBroadcastListener(this.transactionBroadcastListener); - // removePreMessageReceivedEventListener skipped: handlePeerDeath() removes it per-peer on disconnect. - // Calling it here acquires the PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); } if (chainLocksHandler != null) { chainLocksHandler.removeChainLockListener(this.chainLockListener); diff --git a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java index ce692d091..5a3edc57a 100644 --- a/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java +++ b/core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java @@ -788,7 +788,7 @@ public double getMixingProgress() { } totalInputs.addAndGet(1); } else { - if (rounds >= 0) { + if (roundsMixed >= 0) { totalInputs.addAndGet(1); double percentMixedForInput = ((double) roundsMixed) / requiredRounds; totalMixed.addAndGet(percentMixedForInput); diff --git a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java index 9f15c93a3..e86d443e8 100644 --- a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java @@ -94,11 +94,11 @@ public void balanceAndMixingProgressTest() { Stopwatch watch2 = Stopwatch.createStarted(); assertEquals(1.00, wallet.getCoinJoin().getMixingProgress2(), 0.001); - info("getMixingProgress: {}", watch2); + info("getMixingProgress2: {}", watch2); Stopwatch watch3 = Stopwatch.createStarted(); assertEquals(0.9864790925660492, wallet.getCoinJoin().getMixingProgress(), 0.001); - info("getMixingProgress2: {}", watch3); + info("getMixingProgress: {}", watch3); } @Test From 3f7e97c88a67a7b7dafda0703756a0d2d35597de Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 5 May 2026 20:12:29 -0700 Subject: [PATCH 08/13] fix: restore removal of peergroup listeners in CoinJoinManager --- .../org/bitcoinj/coinjoin/utils/CoinJoinManager.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java index 59b56c537..a2cdd3701 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -290,9 +290,12 @@ public void close() { blockChain.removeNewBestBlockListener(newBestBlockListener); blockChain.removeTransactionReceivedListener(transactionReceivedInBlockListener); } - // removePreMessageReceivedEventListener skipped on both peerGroup and masternodeGroup: - // handlePeerDeath() removes it per-peer on disconnect. Calling it here acquires the - // PeerGroup lock via getConnectedPeers(), risking shutdown deadlock. + if (peerGroup != null) { + peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } + if (masternodeGroup != null) { + masternodeGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener); + } // Shut down the executor before nulling fields — queued tasks may still reference them. ExecutorService execToStop = null; From 2dbcf72e104e5ddf27ec0e3816e65d6b9d6ea738 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 5 May 2026 20:16:58 -0700 Subject: [PATCH 09/13] fix: add check for null blockChain --- core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java index 0e0535f91..d12a7c668 100644 --- a/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java +++ b/core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java @@ -208,6 +208,7 @@ void processNewChainLock(Peer from, ChainLockSignature clsig, Sha256Hash hash) { lock.lock(); try { + if (blockChain == null) return; // re-check under lock if (seenChainLocks.put(hash, Utils.currentTimeMillis()) != null) { return; } From 0a6011b189ad6ec5a13bcd195b0924b311ce5218 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 5 May 2026 20:19:38 -0700 Subject: [PATCH 10/13] fix: improve DumpMasternodeList processing of mnlistdiff message --- .../java/org/bitcoinj/examples/DumpMasternodeList.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java b/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java index 68105c63b..3a9d84019 100644 --- a/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java +++ b/examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java @@ -81,10 +81,9 @@ public static void main(String[] args) throws BlockStoreException, ExecutionExce if (m instanceof SimplifiedMasternodeListDiff) { System.out.println("Received mnlistdiff..."); File dumpFile = new File(params.getNetworkName() + "-mnlist.dat"); - OutputStream stream = new FileOutputStream(dumpFile); - stream.write(m.bitcoinSerialize()); - stream.close(); - mnlistdiffReceivedFuture.set(true); + try (OutputStream stream = new FileOutputStream(dumpFile)) { + stream.write(m.bitcoinSerialize()); + } SimplifiedMasternodeListDiff diff = (SimplifiedMasternodeListDiff)m; AtomicInteger countLegacy = new AtomicInteger(); AtomicInteger countEnabled = new AtomicInteger(); @@ -102,14 +101,17 @@ public static void main(String[] args) throws BlockStoreException, ExecutionExce System.out.printf("Total: %d, Legacy: %d\n", countEnabled.get(), countLegacy.get()); System.out.println("HP Masternode List"); evoNodes.forEach(entry -> System.out.println("\"" + entry.getService().getAddr().getHostAddress() + "\",")); + mnlistdiffReceivedFuture.set(true); return null; } } catch (FileNotFoundException e) { System.out.println("cannot find the file to write to"); + mnlistdiffReceivedFuture.setException(e); throw new RuntimeException(e); } catch (IOException e) { System.out.println("IO Error"); e.printStackTrace(); + mnlistdiffReceivedFuture.setException(e); throw new RuntimeException(e); } From 3414bba6bfee386fe3d7f0cb3044fb6f301a2d6c Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 5 May 2026 23:53:25 -0700 Subject: [PATCH 11/13] fix: set some logging to debug in CoinJoinManager --- .../java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java index a2cdd3701..872fda4ad 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -550,12 +550,12 @@ public void processTransaction(Transaction tx) { if(!alreadyHave(item)) { getdata.addItem(item); } else { - log.info("coinjoin: DSQUEUE: already has {}", item.hash); + log.debug("coinjoin: DSQUEUE: already has {}", item.hash); } } if (!getdata.getItems().isEmpty()) { // This will cause us to receive a bunch of block or tx messages. - log.info(COINJOIN_EXTRA, "coinjoin: DSQUEUE: requesting {} dsq messages", getdata.getItems().size()); + log.debug(COINJOIN_EXTRA, "coinjoin: DSQUEUE: requesting {} dsq messages", getdata.getItems().size()); getdata.getItems().forEach( inventoryItem -> log.info(COINJOIN_EXTRA, "getdata: {}", inventoryItem.hash)); peer.sendMessage(getdata); From f7430870f33b67788e413658da2752ecdfab4f30 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 20 May 2026 07:57:57 -0700 Subject: [PATCH 12/13] tests: fix LargeCoinJoinWalletTest --- .../test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java index e86d443e8..4ca92e8f0 100644 --- a/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java @@ -97,7 +97,7 @@ public void balanceAndMixingProgressTest() { info("getMixingProgress2: {}", watch2); Stopwatch watch3 = Stopwatch.createStarted(); - assertEquals(0.9864790925660492, wallet.getCoinJoin().getMixingProgress(), 0.001); + assertEquals(0.9987573607826772, wallet.getCoinJoin().getMixingProgress(), 0.001); info("getMixingProgress: {}", watch3); } From 3119cb6c3010d7f533a84d96971b94dcd8fb42e5 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 20 May 2026 09:02:05 -0700 Subject: [PATCH 13/13] chore: update tests CI to support MacOS latest --- .github/workflows/tests.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edab10153..c49d9a2f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,14 +14,16 @@ jobs: fail-fast: false name: JAVA ${{ matrix.distribution }} ${{ matrix.java }} OS ${{ matrix.os }} Gradle ${{ matrix.gradle }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: ${{ matrix.java }} - name: Build bls library run: | - git submodule update --init --recursive cd contrib/dashj-bls git apply catch_changes.patch mvn package -DskipTests -Dmaven.javadoc.skip=true