Skip to content

Fix/refresh unused keys#296

Open
HashEngineering wants to merge 6 commits intomasterfrom
fix/refresh-unused-keys
Open

Fix/refresh unused keys#296
HashEngineering wants to merge 6 commits intomasterfrom
fix/refresh-unused-keys

Conversation

@HashEngineering
Copy link
Copy Markdown
Collaborator

@HashEngineering HashEngineering commented Apr 29, 2026

Issue being fixed or feature implemented

What was done?

How Has This Been Tested?

Breaking Changes

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added blockchain download progress tracking capabilities.
    • Improved mixing progress calculation for CoinJoin operations with enhanced methodology.
    • Added new command-line example application for masternode list operations.
  • Bug Fixes

    • Improved shutdown stability to prevent potential deadlocks in system components.
    • Corrected chain download restart logic to prevent downloads during shutdown sequences.
  • Chores

    • Refactored internal shutdown handling and state management across multiple managers.

@HashEngineering HashEngineering self-assigned this Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

This pull request refactors listener lifecycle management across multiple manager classes to avoid deadlocks during shutdown. Rather than explicitly unregistering peer-group event listeners in close() methods, the code now relies on per-peer cleanup performed in existing handlePeerDeath() handlers. Additionally, it introduces download-progress tracking to DualBlockChain, improves efficiency in CoinJoinExtension's mixing-progress calculation, and adds a new DumpMasternodeList example utility.

Changes

Cohort / File(s) Summary
Listener Management Refactoring
core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java, core/src/main/java/org/bitcoinj/core/MasternodeSync.java, core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java, core/src/main/java/org/bitcoinj/governance/GovernanceManager.java, core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java, core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java
Removed explicit listener unregistration from PeerGroup in close() methods to prevent potential deadlocks; rely instead on per-peer cleanup in disconnect handlers. Added explicit nulling of peerGroup and blockChain references during shutdown.
DualBlockChain Progress Tracking
core/src/main/java/org/bitcoinj/core/DualBlockChain.java
Added custom DownloadProgressTracker subclass to monitor header and block download completion. Introduced setPeerGroup() to wire tracker to PeerGroup events, isInitialHeaderSyncComplete() to expose sync status, and close() for cleanup.
PeerGroup & BlockChain Coordination
core/src/main/java/org/bitcoinj/core/AbstractManager.java, core/src/main/java/org/bitcoinj/core/PeerGroup.java, core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java, core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java, core/src/main/java/org/bitcoinj/manager/DashSystem.java
Added Javadoc clarifications, updated download restart logic to check both downloadListener and vRunning state, added null guards in event handlers to prevent processing during shutdown, and integrated persistent DualBlockChain ownership in DashSystem.
SporkManager Refactoring
core/src/main/java/org/bitcoinj/core/SporkManager.java
Changed close() signature to remove PeerGroup parameter; added private peerGroup field set during setBlockChain(). Added null-check guard in processSpork() to prevent execution post-closure.
CoinJoinExtension & Testing
core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java, core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
Optimized refreshUnusedKeys() with pre-computed pubkey-hash set for constant-time lookups. Redesigned getMixingProgress() to track weighted per-output progress using AtomicDouble; preserved old logic as deprecated getMixingProgress2(). Updated test to validate both progress methods.
New Example Application
examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java
Added CLI utility to fetch and dump SimplifiedMasternodeListDiff messages, write serialized data to file, and report masternode counts and host IPs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With careful hops through listener lanes,
We dodge the deadlock's tricky chains,
Per-peer cleanup keeps us spry,
While progress trackers watch on high—
Mix metrics flow with grace so neat,
BitcoinJ's shutdown now complete!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/refresh unused keys' is vague and does not clearly reflect the actual scope of changes, which span peer lifecycle management, mixing progress calculation, and a new masternode example. Consider a more specific title that captures the primary change, such as 'Refactor shutdown sequence and improve unused keys refresh' or breaking into multiple PRs if changes are too broad.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/refresh-unused-keys

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@HashEngineering
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
core/src/main/java/org/bitcoinj/manager/DashSystem.java (1)

255-297: ⚠️ Potential issue | 🟠 Major

Guard dualBlockChain.close() against null initialization.

The field dualBlockChain is declared at line 61 but only assigned in setPeerGroupAndBlockChain() (line 304). However, setMasternodeListManager() (line 156) creates a local DualBlockChain variable and never assigns it to the field. If close() is called after only using setMasternodeListManager(), the call to dualBlockChain.close() at line 293 will NPE.

Either assign the field in setMasternodeListManager() by changing line 156 to this.dualBlockChain = ..., or add a null-check before closing:

if (dualBlockChain != null) {
    dualBlockChain.close();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/manager/DashSystem.java` around lines 255 -
297, The close() method calls dualBlockChain.close() unguarded which can NPE if
dualBlockChain was never assigned (it’s only set in setPeerGroupAndBlockChain()
and setMasternodeListManager() currently creates a local DualBlockChain). Fix by
either assigning the field in setMasternodeListManager() (change the local
instantiation to set this.dualBlockChain = ...) or by adding a null-check in
close() before calling dualBlockChain.close() (if (dualBlockChain != null) {
dualBlockChain.close(); }), referencing the dualBlockChain field,
setMasternodeListManager(), setPeerGroupAndBlockChain(), and close().
core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java (1)

444-468: ⚠️ Potential issue | 🟠 Major

Take a local snapshot of blockChain before using it.

The blockChain field is not volatile and isSynced() does not synchronize. Since shutdown code nullifies blockChain, a race condition exists where blockChain can transition from non-null to null between the checks at lines 449 and the subsequent calls at lines 452, 460. Capturing a local snapshot prevents NPE during concurrent shutdown.

🛠️ Suggested fix
+        DualBlockChain currentBlockChain = blockChain;
-        if (blockChain != null && !params.isDIP0024Active(blockChain.getBestChainHeight()))
+        if (currentBlockChain != null && !params.isDIP0024Active(currentBlockChain.getBestChainHeight()))
             return true;

         if (mnListAtH.getHeight() == -1)
             return false;

-        if (blockChain == null)
+        if (currentBlockChain == null)
             return false;

-        if (!blockChain.isInitialHeaderSyncComplete()) {
+        if (!currentBlockChain.isInitialHeaderSyncComplete()) {
             return false;
         }

-        int mostCommonHeight = blockChain.getBestChainHeight();
+        int mostCommonHeight = currentBlockChain.getBestChainHeight();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java` around
lines 444 - 468, isSynced() races on the non-volatile field blockChain; capture
a local snapshot (e.g., BlockChain chain = this.blockChain) at the start of the
method and use that local variable for all subsequent calls
(chain.getBestChainHeight(), chain.isInitialHeaderSyncComplete(), and passing to
params.isDIP0024Active) and for the null checks so the method cannot NPE if
shutdown nulls the field concurrently; ensure you return the same results by
checking snapshot == null where appropriate before using its methods.
core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java (1)

129-137: ⚠️ Potential issue | 🟠 Major

Capture blockChain reference at method entry to prevent concurrent nulling during shutdown.

The early null check at line 206 is insufficient. Between the lock release (line 220) and re-acquisition (line 255), close() can execute and null blockChain without holding the lock. This creates a window where line 269's dereference will fail.

🔧 Suggested fix
 void processNewChainLock(Peer from, ChainLockSignature clsig, Sha256Hash hash) {
-    if (blockChain == null) return; // closed during shutdown
+    AbstractBlockChain currentBlockChain = blockChain;
+    if (currentBlockChain == null) return; // closed during shutdown
 
     lock.lock();
     try {
@@ -269 +269 @@
-                StoredBlock block = blockChain.getBlockStore().get(clsig.blockHash);
+                StoredBlock block = currentBlockChain.getBlockStore().get(clsig.blockHash);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java` around lines
129 - 137, Capture the current blockChain field into a local final variable at
the start of the method that performs the early null check (in class
ChainLocksHandler, the method that references blockChain between the early check
and the dereference) to prevent close() from nulling the field mid-execution;
i.e., create a local variable (e.g., final BlockChain localChain =
this.blockChain) immediately after method entry and use localChain for all
subsequent null checks and dereferences instead of referencing the field
directly, leaving the close() method unchanged.
core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java (1)

122-149: ⚠️ Potential issue | 🟠 Major

close() must synchronize field nulling with ongoing callbacks or add null guards.

preMessageReceivedEventListener is intentionally not unregistered in close() (to avoid deadlock), but remains active and can invoke processInstantSendLock(). This method accesses blockChain (via blockChain.getBlockStore().get() and blockChain.getChainHead()) before acquiring the lock, exposing a race with close() nulling these fields without synchronization. A concurrent callback will hit NullPointerException.

Either:

  1. Acquire the lock earlier in processInstantSendLock() before accessing blockChain, or
  2. Add null checks before field access, or
  3. Synchronize close() with pending callbacks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java` around lines
122 - 149, The race is that preMessageReceivedEventListener can call
processInstantSendLock() which reads blockChain before the instance lock is held
while close() can null blockChain; to fix, start processInstantSendLock() by
acquiring the existing lock (the same lock used to protect instance state)
before any access to blockChain, check if blockChain is null and return if so,
and release the lock in a finally block; this ensures callbacks via
preMessageReceivedEventListener cannot observe partially-closed state even
though close() does not unregister that listener.
core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java (1)

288-313: ⚠️ Potential issue | 🟠 Major

close() never cancels the scheduled maintenance task before nulling collaborators.

This method nulls blockChain and peerGroup, but it never cancels schedule, which means doMaintenance() can still be scheduled to run against null fields. The stop() method properly cancels schedule and shuts down the message executor with awaitTermination(), but close() skips this critical cleanup. If called without stop() being called first, doMaintenance() could reference null fields.

Additionally, close() calls execToStop.shutdown() without waiting for termination, while stop() properly uses awaitTermination(10, TimeUnit.SECONDS). Reuse the existing stop() logic before clearing shared state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java` around
lines 288 - 313, The close() method must cancel the scheduled maintenance and
wait for the message executor to terminate before nulling collaborators; update
close() to mirror stop() behavior by cancelling the schedule (future returned
from schedule), ensuring any scheduled doMaintenance() is cancelled, and
invoking the same shutdown + awaitTermination logic used in stop() for
messageProcessingExecutor (instead of only shutdown()), then clear blockChain
and peerGroup; reference the existing stop(), close(), schedule (the
ScheduledFuture), doMaintenance(), and messageProcessingExecutor symbols so the
cleanup sequence is performed safely.
core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1)

551-582: ⚠️ Potential issue | 🟠 Major

The precomputed transaction snapshot can resurrect a used key.

wallet.getTransactions(true) is only a snapshot, and processTransaction() updates unusedKeys/keyUsage without taking unusedKeysLock. If a transaction arrives after Lines 553-561 but before this rebuild completes, refreshUnusedKeys() can overwrite that newer state and put the just-used key back into unusedKeys, which risks address reuse. This refresh needs the same synchronization as the live transaction updates, or a final revalidation step before committing the rebuilt maps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java` around lines
551 - 582, The rebuild can race with processTransaction and resurrect a
just-used key; to fix, ensure the precomputed usedPubKeyHashes is validated
while holding the same lock used for committing the rebuilt maps: compute or
recompute usedPubKeyHashes after acquiring unusedKeysLock (or, alternatively,
after acquiring unusedKeysLock re-scan wallet.getTransactions(true) to add any
new outputs) before mutating unusedKeys and keyUsage; keep references to
unusedKeysLock, wallet.getTransactions(true), usedPubKeyHashes, unusedKeys,
keyUsage, issuedKeys and processTransaction so reviewers can find and verify the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/main/java/org/bitcoinj/core/DualBlockChain.java`:
- Around line 134-155: The close() method currently just nulls peerGroup leaving
downloadProgressTracker registered; update close() to detach
downloadProgressTracker from the existing peerGroup by calling the corresponding
remove...EventListener/remove...Listener methods (the same ones used in
setPeerGroup: removeHeadersDownloadedEventListener,
removeHeadersDownloadStartedEventListener, removeBlocksDownloadedEventListener,
removeChainDownloadStartedEventListener, removeMasternodeListDownloadListener
with Threading.USER_THREAD and the stored downloadProgressTracker) before
nulling peerGroup and clearing downloadProgressTracker; also modify
setPeerGroup(PeerGroup, MasternodeSync) to check for an existing peerGroup +
downloadProgressTracker and remove the old tracker from that peerGroup first (to
avoid duplicate registrations) before assigning the new peerGroup and
creating/adding a fresh MyDownloadProgressTracker.

In `@core/src/main/java/org/bitcoinj/core/SporkManager.java`:
- Around line 107-116: close() currently nulls the PeerGroup reference without
unregistering the PeerGroup-level listener, leaving peerConnectedEventListener
registered and causing future GetSporksMessage calls or duplicate callbacks when
setBlockChain() is called; fix by first calling
peerGroup.removeConnectedEventListener(peerConnectedEventListener) (and nulling
peerConnectedEventListener) before setting peerGroup = null, and also add a
guard in setBlockChain() to avoid re-registering the same
peerConnectedEventListener if it's already attached.

In
`@core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java`:
- Around line 454-457: The shutdown sequence in SimplifiedMasternodeListManager
nulls blockChain before calling saveNow(), causing saveNow() to operate without
the blockchain state; reorder the operations in the close method so you call
saveNow() while blockChain (and peerGroup) are still valid, then set peerGroup =
null and blockChain = null, and finally call super.close(); this ensures
saveNow() persists the correct quorum/list state.

In `@core/src/main/java/org/bitcoinj/governance/GovernanceManager.java`:
- Around line 1718-1721: close() currently only nulls the peerGroup reference
but leaves this manager registered as a PeerGroup listener
(getDataEventListener, preMessageReceivedEventListener), so it can still receive
callbacks; before setting peerGroup = null, explicitly unregister the listeners
by calling peerGroup.removeDataEventListener(getDataEventListener()) and
peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener)
(or the corresponding remove* methods present on PeerGroup) and handle/ignore
any exceptions or early-return if peerGroup is already shutting down to avoid
deadlock; after successful removal, then set peerGroup = null so no lingering
references remain.

In `@core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java`:
- Around line 783-795: The partial-progress branch is incorrectly guarded by
"rounds >= 0" causing outputs with negative roundsMixed (from
getRealOutpointCoinJoinRounds()) to be counted and add negative values to
totalMixed; change the condition to check "roundsMixed >= 0" before incrementing
totalInputs and adding percentMixedForInput so only valid per-output progress
contributes to totalInputs/totalMixed (use roundsMixed, requiredRounds,
totalInputs, totalMixed, and the getRealOutpointCoinJoinRounds() result to
locate the logic in CoinJoinExtension).

In `@core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java`:
- Around line 95-101: The two info() calls log the wrong getter names for the
measured times — swap the log labels so the Stopwatch stored in watch2 is logged
with "getMixingProgress2" and the Stopwatch in watch3 is logged with
"getMixingProgress"; update the info(...) invocations that follow the assertions
around wallet.getCoinJoin().getMixingProgress2() and
wallet.getCoinJoin().getMixingProgress() accordingly so each info() references
the matching getter name.

In `@examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java`:
- Around line 61-68: The switch on the network string in DumpMasternodeList
currently falls through to MainNetParams on unknown input; change it to
explicitly handle only "testnet" and "mainnet" (or "main") and throw an
IllegalArgumentException (or print usage and exit) for any other value so typos
don't silently default to MainNetParams.get(); update the switch (or if/else)
around the variable params and use TestNet3Params.get() and MainNetParams.get()
only for recognized tokens and fail fast for unrecognized network names.
- Around line 79-114: The handler passed to
peerGroup.addPreMessageReceivedEventListener currently sets
mnlistdiffReceivedFuture early and manually closes the OutputStream; instead,
wrap the FileOutputStream in a try-with-resources and move
mnlistdiffReceivedFuture.set(true) to after all processing (writing,
diff.getMnList iteration and other post-processing) completes successfully so
the future is only completed on success; ensure exceptions still propagate (or
complete exceptionally) and do not close the future prematurely when handling
SimplifiedMasternodeListDiff in the lambda that casts to
SimplifiedMasternodeListDiff and uses stream, diff, countLegacy/countEnabled and
evoNodes.

---

Outside diff comments:
In `@core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java`:
- Around line 288-313: The close() method must cancel the scheduled maintenance
and wait for the message executor to terminate before nulling collaborators;
update close() to mirror stop() behavior by cancelling the schedule (future
returned from schedule), ensuring any scheduled doMaintenance() is cancelled,
and invoking the same shutdown + awaitTermination logic used in stop() for
messageProcessingExecutor (instead of only shutdown()), then clear blockChain
and peerGroup; reference the existing stop(), close(), schedule (the
ScheduledFuture), doMaintenance(), and messageProcessingExecutor symbols so the
cleanup sequence is performed safely.

In `@core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java`:
- Around line 444-468: isSynced() races on the non-volatile field blockChain;
capture a local snapshot (e.g., BlockChain chain = this.blockChain) at the start
of the method and use that local variable for all subsequent calls
(chain.getBestChainHeight(), chain.isInitialHeaderSyncComplete(), and passing to
params.isDIP0024Active) and for the null checks so the method cannot NPE if
shutdown nulls the field concurrently; ensure you return the same results by
checking snapshot == null where appropriate before using its methods.

In `@core/src/main/java/org/bitcoinj/manager/DashSystem.java`:
- Around line 255-297: The close() method calls dualBlockChain.close() unguarded
which can NPE if dualBlockChain was never assigned (it’s only set in
setPeerGroupAndBlockChain() and setMasternodeListManager() currently creates a
local DualBlockChain). Fix by either assigning the field in
setMasternodeListManager() (change the local instantiation to set
this.dualBlockChain = ...) or by adding a null-check in close() before calling
dualBlockChain.close() (if (dualBlockChain != null) { dualBlockChain.close();
}), referencing the dualBlockChain field, setMasternodeListManager(),
setPeerGroupAndBlockChain(), and close().

In `@core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java`:
- Around line 129-137: Capture the current blockChain field into a local final
variable at the start of the method that performs the early null check (in class
ChainLocksHandler, the method that references blockChain between the early check
and the dereference) to prevent close() from nulling the field mid-execution;
i.e., create a local variable (e.g., final BlockChain localChain =
this.blockChain) immediately after method entry and use localChain for all
subsequent null checks and dereferences instead of referencing the field
directly, leaving the close() method unchanged.

In `@core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java`:
- Around line 122-149: The race is that preMessageReceivedEventListener can call
processInstantSendLock() which reads blockChain before the instance lock is held
while close() can null blockChain; to fix, start processInstantSendLock() by
acquiring the existing lock (the same lock used to protect instance state)
before any access to blockChain, check if blockChain is null and return if so,
and release the lock in a finally block; this ensures callbacks via
preMessageReceivedEventListener cannot observe partially-closed state even
though close() does not unregister that listener.

In `@core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java`:
- Around line 551-582: The rebuild can race with processTransaction and
resurrect a just-used key; to fix, ensure the precomputed usedPubKeyHashes is
validated while holding the same lock used for committing the rebuilt maps:
compute or recompute usedPubKeyHashes after acquiring unusedKeysLock (or,
alternatively, after acquiring unusedKeysLock re-scan
wallet.getTransactions(true) to add any new outputs) before mutating unusedKeys
and keyUsage; keep references to unusedKeysLock, wallet.getTransactions(true),
usedPubKeyHashes, unusedKeys, keyUsage, issuedKeys and processTransaction so
reviewers can find and verify the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3daa7559-5da2-4824-8fde-8e4964819b6b

📥 Commits

Reviewing files that changed from the base of the PR and between 5d9b0c9 and 28992b7.

📒 Files selected for processing (16)
  • core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java
  • core/src/main/java/org/bitcoinj/core/AbstractManager.java
  • core/src/main/java/org/bitcoinj/core/DualBlockChain.java
  • core/src/main/java/org/bitcoinj/core/MasternodeSync.java
  • core/src/main/java/org/bitcoinj/core/PeerGroup.java
  • core/src/main/java/org/bitcoinj/core/SporkManager.java
  • core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java
  • core/src/main/java/org/bitcoinj/evolution/QuorumRotationState.java
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java
  • core/src/main/java/org/bitcoinj/governance/GovernanceManager.java
  • core/src/main/java/org/bitcoinj/manager/DashSystem.java
  • core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java
  • core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java
  • core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
  • examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java

Comment on lines +134 to +155
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;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

close() is leaving a PeerGroup-wide tracker registered.

The listeners added in Lines 138-142 are attached to PeerGroup, not to individual peers, so the per-peer shutdown reasoning used elsewhere does not apply here. close() only nulls peerGroup, which leaves downloadProgressTracker registered on the old group and able to keep receiving callbacks after shutdown; calling setPeerGroup() again would also register an additional tracker. This needs explicit detach/replace handling around the stored tracker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/core/DualBlockChain.java` around lines 134 -
155, The close() method currently just nulls peerGroup leaving
downloadProgressTracker registered; update close() to detach
downloadProgressTracker from the existing peerGroup by calling the corresponding
remove...EventListener/remove...Listener methods (the same ones used in
setPeerGroup: removeHeadersDownloadedEventListener,
removeHeadersDownloadStartedEventListener, removeBlocksDownloadedEventListener,
removeChainDownloadStartedEventListener, removeMasternodeListDownloadListener
with Threading.USER_THREAD and the stored downloadProgressTracker) before
nulling peerGroup and clearing downloadProgressTracker; also modify
setPeerGroup(PeerGroup, MasternodeSync) to check for an existing peerGroup +
downloadProgressTracker and remove the old tracker from that peerGroup first (to
avoid duplicate registrations) before assigning the new peerGroup and
creating/adding a fresh MyDownloadProgressTracker.

Comment on lines +107 to +116
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Detach the PeerGroup-level connect listener before dropping the reference.

Skipping preMessageReceivedEventListener cleanup makes sense because that one is removed per peer, but peerConnectedEventListener is registered on the PeerGroup itself in Lines 96-98. close() now nulls peerGroup without unregistering it, so this SporkManager can stay reachable and still send GetSporksMessage on later connections; a subsequent setBlockChain() call would also stack duplicate callbacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/core/SporkManager.java` around lines 107 -
116, close() currently nulls the PeerGroup reference without unregistering the
PeerGroup-level listener, leaving peerConnectedEventListener registered and
causing future GetSporksMessage calls or duplicate callbacks when
setBlockChain() is called; fix by first calling
peerGroup.removeConnectedEventListener(peerConnectedEventListener) (and nulling
peerConnectedEventListener) before setting peerGroup = null, and also add a
guard in setBlockChain() to avoid re-registering the same
peerConnectedEventListener if it's already attached.

Comment on lines +1718 to +1721
// 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

close() drops the reference but does not detach registered listeners.

Lines 1718-1721 only null the local peerGroup. The PeerGroup listener registries still hold getDataEventListener / preMessageReceivedEventListener, so this manager can remain referenced and continue receiving callbacks if peers are still active.

💡 Proposed fix
 `@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.
-    peerGroup = null;
+    PeerGroup pg = peerGroup;
+    peerGroup = null;
+    if (pg != null && !pg.isRunning()) {
+        pg.removeGetDataEventListener(getDataEventListener);
+        pg.removePreMessageReceivedEventListener(preMessageReceivedEventListener);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/bitcoinj/governance/GovernanceManager.java` around
lines 1718 - 1721, close() currently only nulls the peerGroup reference but
leaves this manager registered as a PeerGroup listener (getDataEventListener,
preMessageReceivedEventListener), so it can still receive callbacks; before
setting peerGroup = null, explicitly unregister the listeners by calling
peerGroup.removeDataEventListener(getDataEventListener()) and
peerGroup.removePreMessageReceivedEventListener(preMessageReceivedEventListener)
(or the corresponding remove* methods present on PeerGroup) and handle/ignore
any exceptions or early-return if peerGroup is already shutting down to avoid
deadlock; after successful removal, then set peerGroup = null so no lingering
references remain.

Comment thread core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
Comment thread core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
Comment on lines +79 to +114
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<Masternode> 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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't signal success before the dump is fully processed.

mnlistdiffReceivedFuture is completed before the post-processing finishes, and the file stream is closed manually. If anything fails on this path, main() can block forever on get() or proceed as if the dump succeeded. Use try-with-resources and only complete the future after all processing has succeeded.

🛠 Suggested fix
                 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;
+                    try (OutputStream stream = new FileOutputStream(dumpFile)) {
+                        stream.write(m.bitcoinSerialize());
+                    }
+                    SimplifiedMasternodeListDiff diff = (SimplifiedMasternodeListDiff) m;
                     AtomicInteger countLegacy = new AtomicInteger();
                     AtomicInteger countEnabled = new AtomicInteger();
                     ArrayList<Masternode> evoNodes = new ArrayList<>();
                     diff.getMnList().forEach(entry -> {
                         if (entry.isValid()) {
@@
                     });
                     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;
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/src/main/java/org/bitcoinj/examples/DumpMasternodeList.java` around
lines 79 - 114, The handler passed to
peerGroup.addPreMessageReceivedEventListener currently sets
mnlistdiffReceivedFuture early and manually closes the OutputStream; instead,
wrap the FileOutputStream in a try-with-resources and move
mnlistdiffReceivedFuture.set(true) to after all processing (writing,
diff.getMnList iteration and other post-processing) completes successfully so
the future is only completed on success; ensure exceptions still propagate (or
complete exceptionally) and do not close the future prematurely when handling
SimplifiedMasternodeListDiff in the lambda that casts to
SimplifiedMasternodeListDiff and uses stream, diff, countLegacy/countEnabled and
evoNodes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant