Skip to content

Conversation

@HashEngineering
Copy link
Collaborator

@HashEngineering HashEngineering commented Nov 7, 2025

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

    • Timeout error listener API with propagated notifications; forceStop(waitMs) and saveOnNextBlock toggles.
  • Improvements

    • Adaptive I/O buffering for DB and wallet serialization; richer timing/metrics (including CoinJoin).
    • Non-blocking, timed shutdowns for background pools.
    • More informative logs (thread names, file sizes).
  • Bug Fixes

    • Null-safety guards and wallet thread-safety improvements; faster double-spend lookups.
  • Tests

    • Force-stop tests and expanded wallet performance/consistency tests.

✏️ Tip: You can customize this high-level summary in your review settings.

@HashEngineering HashEngineering self-assigned this Nov 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Walkthrough

Added timeout error types and listener plumbing, executor-introspection and forceStop lifecycle control with max-stalls config, adaptive I/O buffering for DB/wallet serialization, wallet spent-outpoint indexing and save controls, plus assorted robustness tweaks and tests.

Changes

Cohort / File(s) Summary
Timeout Listener Infrastructure
core/src/main/java/org/bitcoinj/core/listeners/TimeoutError.java, core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java, core/src/main/java/org/bitcoinj/core/PeerGroup.java, core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java
Added TimeoutError enum and TimeoutErrorListener interface. PeerGroup and PeerSocketHandler gained registration/removal APIs, listener storage, queuing, wiring to notify listeners, and stack-inspection to classify blockstore-related timeouts.
Lifecycle & Stall Management
core/src/main/java/org/bitcoinj/core/PeerGroup.java, core/src/test/java/org/bitcoinj/core/PeerGroupTest.java
Added forceStop(int) with executor introspection and timed shutdown logging; exposed setMaxStalls(int)/getMaxStalls() and integrated max-stalls into stall handling. Added unit tests for forceStop behavior.
I/O Buffering & Adaptive Sizing
core/src/main/java/org/bitcoinj/store/FlatDB.java, core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java, core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
Introduced configurable ioBufferSize and useAdaptiveBufferSizing and calculateOptimalBufferSize tiers. Read/write paths now use buffered streams / sized CodedOutputStream with phase timing and logging.
Wallet Indexing & Save Behavior
core/src/main/java/org/bitcoinj/wallet/Wallet.java, core/src/main/java/org/bitcoinj/wallet/WalletEx.java, core/src/main/java/org/bitcoinj/wallet/WalletFiles.java
Added spentOutpointsIndex for O(I) double-spend lookups; added setSaveOnNextBlock(boolean) / getSaveOnNextBlock(); made countInputsWithAmount thread-safe; updated save logging to include file size/reference.
Evolution Robustness & Shutdowns
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java, core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java, core/src/main/java/org/bitcoinj/evolution/QuorumState.java, core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListDiff.java, core/src/main/java/org/bitcoinj/quorums/SimplifiedQuorumList.java
Replaced blocking awaitTermination with non-blocking shutdown/shutdownNow; added null-safety guards and warnings for missing ancestor/modifier; updated toString and minor comment fix.
Tests, Examples & Logging
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java, core/src/test/java/org/bitcoinj/core/PeerGroupTest.java, examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java, core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java, core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java
Added performance/consistency tests (adaptive vs fixed buffers), PeerGroup forceStop tests, optional --debuglog toggle in example, switched BriefLogFormatter to thread name, and adjusted expected key count in a protobuf test.
DashSystem close logging
core/src/main/java/org/bitcoinj/manager/DashSystem.java
Expanded logging and added explicit blocks/braces around resource close steps for clearer lifecycle logs.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Peer as Peer (socket)
    participant Handler as PeerSocketHandler
    participant PG as PeerGroup
    participant Exec as Listener Executor
    participant L as TimeoutErrorListener

    Peer->>Handler: socket timeout event
    Handler->>Handler: capture thread & inspect stacks
    Handler->>Handler: checkForBlockStoreTimeout()
    alt blockstore-related detected
        Handler->>PG: queueTimeoutErrorListeners(BLOCKSTORE_MEMORY_ACCESS, peerAddress)
    else
        Handler->>PG: queueTimeoutErrorListeners(UNKNOWN, peerAddress)
    end
    PG->>PG: iterate listener registrations
    PG->>Exec: submit onTimeout(listener, error, peerAddress)
    Exec->>L: onTimeout(error, peerAddress)
    Handler->>Peer: close connection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • PeerGroup executor introspection, forceStop timing and potential races.
    • Listener lifecycle concurrency: registration/removal and executor submission semantics.
    • Adaptive buffer sizing thresholds and fallback behavior for extreme sizes.
    • spentOutpointsIndex updates across transaction mutation, kill/rollback, and reset paths.
    • LargeCoinJoinWalletTest: long-running/flaky test risk and environment-sensitive assertions.

Poem

A rabbit taps the timeout bell with care, 🐇
Listeners murmur warnings on the air,
Buffers stretch and shrink to bear the load,
Indexes map tracks where double-spends go,
Timed goodbyes hum softly as sockets close.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.96% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'fix: resolve sync issues' is vague and generic, using non-descriptive terms that do not convey meaningful information about the changeset. Replace with a more specific title that summarizes the main changes, such as 'Add timeout error listener infrastructure and improve peer group shutdown handling' or identify the primary fix being addressed.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sync-issues-coinjoin

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

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

Copy link

@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: 6

Caution

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

⚠️ Outside diff range comments (3)
examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java (2)

44-47: Critical: Missing exit after error message causes runtime exception.

When args.length < 1, the error message is printed but the program continues executing, leading to an ArrayIndexOutOfBoundsException at line 53 when attempting to access args[0].

Apply this diff to exit after printing the error:

         if (args.length < 1) {
             System.out.println("RestoreFromSeedThenDump network \"seed phrase\" [--debuglog]");
             System.out.println("  missing the network");
+            return;
         }

65-68: Critical: Missing exit after error message causes runtime exception.

When args.length <= 1, the error message is printed but the program continues executing, leading to an ArrayIndexOutOfBoundsException at line 69 when attempting to access args[1].

Apply this diff to exit after printing the error:

         if (args.length <= 1) {
             System.out.println("RestoreFromSeedThenDump network \"seed phrase\" [--debuglog]");
             System.out.println("  missing the seed phrase");
+            return;
         }
core/src/main/java/org/bitcoinj/core/PeerGroup.java (1)

2234-2264: Critical: maxStalls is never reset and has potential race conditions.

The stall logic has several issues:

  1. No reset mechanism: Once maxStalls reaches 0 after repeated stalls (line 2246), stall disconnections are permanently disabled for the lifetime of this PeerGroup instance. This means if network conditions improve later, the system won't recover the ability to disconnect stalled peers.

  2. TOCTOU race condition: Line 2234 reads currentMaxStalls via getMaxStalls(), but between this read and the synchronized block at lines 2244-2250, another thread could call setMaxStalls(), causing inconsistent behavior.

  3. Decrement semantics: The decrement at line 2246 permanently modifies the field, which may not be the intended design. Consider whether this should be a counter that resets, or a persistent configuration.

Consider one of these approaches:

Option 1: Use a separate stall counter that can reset:

 @GuardedBy("lock") private int maxStalls = 3;
+@GuardedBy("lock") private int currentStallCount = 0;

Then in the calculator:

-                    int currentMaxStalls = getMaxStalls();
-                    if (currentMaxStalls <= 0) {
+                    int configuredMaxStalls = getMaxStalls();
+                    if (configuredMaxStalls <= 0) {
                         log.info(statsString + ", stall disabled " + thresholdString);
                     } else if (warmupSeconds > 0) {
                         // ... warmup logic
                     } else if (average < minSpeedBytesPerSec && !waitForPreBlockDownload) {
-                        log.info("{}, STALLED {}, maxStalls: {}", statsString, thresholdString, currentMaxStalls);
+                        log.info("{}, STALLED {}, maxStalls: {}", statsString, thresholdString, configuredMaxStalls);
                         lock.lock();
                         try {
-                            PeerGroup.this.maxStalls--;
-                            currentMaxStalls = PeerGroup.this.maxStalls;
+                            currentStallCount++;
                         } finally {
                             lock.unlock();
                         }
-                        if (currentMaxStalls == 0) {
+                        if (currentStallCount >= configuredMaxStalls) {
                             log.warn("This network seems to be slower than the requested stall threshold - won't do stall disconnects any more.");
+                            // Optionally reset: currentStallCount = 0;
                         } else {
                             Peer peer = getDownloadPeer();
                             log.warn(String.format(Locale.US,
                                     "Chain download stalled: received %.2f KB/sec for %d seconds, require average of %.2f KB/sec, disconnecting %s, %d stalls left",
-                                    average / 1024.0, samples.length, minSpeedBytesPerSec / 1024.0, peer, currentMaxStalls));
+                                    average / 1024.0, samples.length, minSpeedBytesPerSec / 1024.0, peer, configuredMaxStalls - currentStallCount));

Option 2: Reset the stall count on successful sync or peer change:
Add a method to reset stalls when conditions improve (e.g., when syncDone becomes true at line 2216).

🧹 Nitpick comments (5)
examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java (1)

48-52: Approve conditional logging with suggestions for improvement.

The conditional logging initialization is a useful debugging feature. However, consider these improvements:

  1. Use args.length >= 3 instead of args.length == 3 to support potential future arguments without silently disabling verbose logging.
  2. Document the optional flag: Update the usage messages at lines 45-46 and 66-67 to mention the optional --debuglog parameter.

Apply this diff to make the check more flexible:

-        if (args.length == 3 && args[2].equals("--debuglog")) {
+        if (args.length >= 3 && args[2].equals("--debuglog")) {

Update the usage messages to document the flag:

         if (args.length < 1) {
-            System.out.println("RestoreFromSeedThenDump network \"seed phrase\"");
+            System.out.println("RestoreFromSeedThenDump network \"seed phrase\" [--debuglog]");
             System.out.println("  missing the network");
         if (args.length <= 1) {
-            System.out.println("RestoreFromSeedThenDump network \"seed phrase\"");
+            System.out.println("RestoreFromSeedThenDump network \"seed phrase\" [--debuglog]");
             System.out.println("  missing the seed phrase");
core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1)

617-633: Move return statement outside the finally block.

While the synchronization correctly protects access to myUnspents, placing the return statement inside the finally block is unconventional and can obscure control flow. Store the result in a variable and return it after the finally block completes.

Apply this diff to improve clarity:

     public int countInputsWithAmount(Coin inputValue) {
         lock.lock();
+        int count;
         try {
-            int count = 0;
+            count = 0;
             for (TransactionOutput output : myUnspents) {
                 TransactionConfidence confidence = output.getParentTransaction().getConfidence(context);
                 // confirmations must be 0 or higher, not conflicted or dead
                 if (confidence != null && (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING || confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)) {
                     // inputValue must match, the TX is mine and is not spent
                     if (output.getValue().equals(inputValue) && output.getSpentBy() == null) {
                         count++;
                     }
                 }
             }
-            return count;
         } finally {
             lock.unlock();
         }
+        return count;
     }
core/src/main/java/org/bitcoinj/core/PeerGroup.java (3)

1282-1331: Refactor: Consider moving diagnostic logic to a separate method.

The diagnostic logging in stopAsync() adds significant complexity to a critical shutdown path:

  1. Reflection fragility: Lines 1292-1294 use reflection to access a private delegate field. This will break if Guava changes its internal implementation of ListeningDecorator.
  2. Expensive thread enumeration: Lines 1307-1324 enumerate all threads in the root ThreadGroup, which could include many unrelated threads in a large application. Consider filtering threads more precisely before enumeration.
  3. Broad exception handling: Line 1326 catches all exceptions and only logs the message, potentially masking important diagnostic information.

Consider extracting this diagnostic logic into a separate logExecutorState() method that can be called conditionally (e.g., when a debug flag is enabled), rather than running on every shutdown:

 public ListenableFuture stopAsync() {
     checkState(vRunning);
     vRunning = false;
-    
-    // Log executor state and remaining jobs
-    log.info("stopAsync() called - executor shutdown: {}, terminated: {}", 
-            executor.isShutdown(), executor.isTerminated());
-    
-    // The executor is wrapped by MoreExecutors.listeningDecorator, need to access the underlying executor
-    if (executor instanceof ListeningScheduledExecutorService) {
-        // Try to get the underlying ScheduledThreadPoolExecutor
-        try {
-            // Use reflection to access the delegate field in ListeningDecorator
-            java.lang.reflect.Field delegateField = executor.getClass().getDeclaredField("delegate");
-            delegateField.setAccessible(true);
-            Object delegate = delegateField.get(executor);
-            
-            if (delegate instanceof ScheduledThreadPoolExecutor) {
-                ScheduledThreadPoolExecutor stpe = (ScheduledThreadPoolExecutor) delegate;
-                log.info("Executor queue size: {}, active threads: {}", 
-                        stpe.getQueue().size(), stpe.getActiveCount());
-                
-                // Log remaining jobs in queue
-                stpe.getQueue().forEach(job -> {
-                    log.info("Remaining job: {} (class: {})", job.toString(), job.getClass().getSimpleName());
-                });
-                
-                // Get call stacks of executor threads
-                ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
-                while (rootGroup.getParent() != null) {
-                    rootGroup = rootGroup.getParent();
-                }
-                
-                Thread[] threads = new Thread[rootGroup.activeCount()];
-                rootGroup.enumerate(threads);
-                
-                for (Thread thread : threads) {
-                    if (thread != null && thread.getName().contains("PeerGroup Thread")) {
-                        log.info("Found PeerGroup thread: {} (state: {})", thread.getName(), thread.getState());
-                        StackTraceElement[] stackTrace = thread.getStackTrace();
-                        log.info("Stack trace for thread {}:", thread.getName());
-                        for (int i = 0; i < Math.min(stackTrace.length, 15); i++) {
-                            log.info("  at {}", stackTrace[i]);
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.info("Could not access underlying executor details: {}", e.getMessage());
-        }
-    }
-    
+    log.info("stopAsync() called");
     log.info("About to submit shutdown task to executor");

1373-1401: Add documentation for thread safety and usage guidance.

The forceStop() method provides useful forced-shutdown capability, but lacks documentation about when to use it versus stop() and thread-safety considerations.

Consider adding JavaDoc:

+/**
+ * Performs a forced shutdown of the PeerGroup after waiting for the specified timeout.
+ * Unlike {@link #stop()}, this method will forcibly terminate the executor if it doesn't
+ * shut down within the specified time.
+ * 
+ * <p>This method can be called multiple times safely. If the PeerGroup is not running,
+ * it will still attempt to shut down the executor.</p>
+ * 
+ * <p><b>Thread safety:</b> This method blocks until shutdown completes or times out.</p>
+ * 
+ * @param waitMsBeforeShutdown Maximum time in milliseconds to wait for graceful shutdown
+ *                             before forcing termination
+ */
 public void forceStop(int waitMsBeforeShutdown) {

1403-1417: Consider more robust job identification.

The getJobDescription() method uses string matching on toString() output (lines 1408-1414), which is brittle and could break if the string representation changes.

Consider using instanceof checks or maintaining a Map<Runnable, String> for known jobs:

 private String getJobDescription(Runnable job) {
     if (job == triggerConnectionsJob) {
         return "Peer connection management";
     } else if (job instanceof ChainDownloadSpeedCalculator) {
         return "Chain download speed monitoring";
+    } else if (job instanceof ScheduledFuture) {
+        // Unwrap ScheduledFuture to get the actual task
+        // Note: This requires additional logic to safely unwrap
+        return "Scheduled task";
     } else {
-        return "Unknown task";
+        return "Unknown task: " + job.getClass().getName();
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3e1aee and e9fc4de.

📒 Files selected for processing (17)
  • core/build.gradle (1 hunks)
  • core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java (5 hunks)
  • core/src/main/java/org/bitcoinj/core/Peer.java (6 hunks)
  • core/src/main/java/org/bitcoinj/core/PeerGroup.java (7 hunks)
  • core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1 hunks)
  • core/src/main/java/org/bitcoinj/core/VersionMessage.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1 hunks)
  • core/src/main/java/org/bitcoinj/store/FlatDB.java (5 hunks)
  • core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java (2 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletFiles.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (2 hunks)
  • core/src/test/java/org/bitcoinj/core/PeerGroupTest.java (1 hunks)
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (4 hunks)
  • examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
  • core/src/main/java/org/bitcoinj/wallet/WalletEx.java
  • core/src/main/java/org/bitcoinj/core/Peer.java
  • core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
📚 Learning: 2025-09-18T22:12:09.613Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 286
File: core/src/main/java/org/bitcoinj/wallet/Wallet.java:6371-6373
Timestamp: 2025-09-18T22:12:09.613Z
Learning: In bitcoinj's Wallet.java, methods returning List<ECKey> can safely return new LinkedList<>(detkeys) where detkeys is List<DeterministicKey>, because DeterministicKey extends ECKey and Java's type inference handles this covariance correctly during list construction.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧬 Code graph analysis (4)
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (3)
core/src/main/java/org/bitcoinj/core/Transaction.java (1)
  • Transaction (70-1867)
core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java (1)
  • BriefLogFormatter (29-77)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)
  • Wallet (129-6456)
core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1)
core/src/main/java/org/bitcoinj/core/TransactionConfidence.java (1)
  • TransactionConfidence (66-714)
examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java (1)
core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java (1)
  • BriefLogFormatter (29-77)
core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1)
core/src/main/java/org/bitcoinj/core/Sha256Hash.java (1)
  • Sha256Hash (38-315)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
🔇 Additional comments (15)
core/src/main/java/org/bitcoinj/core/VersionMessage.java (1)

47-47: Version constant update is correct and consistent.

The BITCOINJ_VERSION constant update from "21.1.12-SNAPSHOT" to "21.1.12" is properly synchronized with the version in core/build.gradle. This public constant affects the LIBRARY_SUBVER field used in peer identification strings (line 49), which will now report the release version to network peers.

core/build.gradle (1)

13-13: Version bump verified and correctly synchronized.

The version change from 21.1.12-SNAPSHOT to 21.1.12 is complete and consistent across the codebase. Both core/build.gradle (line 13) and VersionMessage.java (line 47: BITCOINJ_VERSION) reference the same version, with no orphaned SNAPSHOT versions remaining. The git diff confirms this PR includes multiple substantive changes across network communication, wallet operations, and CoinJoin functionality—aligning with the mentioned sync issue fixes.

core/src/main/java/org/bitcoinj/wallet/WalletFiles.java (1)

141-141: LGTM! Enhanced observability for wallet save operations.

The addition of file size and path to the log message provides valuable diagnostic information without impacting functionality.

core/src/main/java/org/bitcoinj/store/FlatDB.java (4)

29-30: LGTM! Configurable I/O buffering with sensible defaults.

The default 64KB buffer size and adaptive sizing flag provide good starting points for performance optimization while maintaining flexibility.


113-135: LGTM! Well-designed adaptive buffer sizing logic.

The tiered buffer sizes (4KB, 16KB, 64KB, 128KB) based on file size are appropriate for balancing memory usage and I/O performance across different file sizes.


163-169: LGTM! Proper use of buffered I/O with try-with-resources.

The write path correctly uses BufferedOutputStream with adaptive buffer sizing and ensures proper resource cleanup through try-with-resources.


218-223: LGTM! Proper use of buffered I/O with try-with-resources.

The read path correctly uses BufferedInputStream with adaptive buffer sizing and ensures proper resource cleanup through try-with-resources.

core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java (4)

53-53: LGTM! Appropriate use of concurrent data structure.

Replacing HashMap with ConcurrentHashMap for mapDSTX eliminates the need for explicit locking while maintaining thread-safety for DSTX management.


224-224: LGTM! Safe concurrent removal of expired entries.

The removeIf operation on ConcurrentHashMap.entrySet() is thread-safe and provides atomic removal per entry, correctly replacing the previous explicit locking approach.


250-265: LGTM! Simplified transaction flow hooks with concurrent map semantics.

Removing explicit synchronization is safe here since ConcurrentHashMap provides thread-safe operations for the individual get/put operations within updateDSTXConfirmedHeight.


321-321: LGTM! Thread-safe status message map.

Using ConcurrentHashMap for statusMessageMap is appropriate, as it's initialized in the static block and accessed concurrently for read operations.

core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java (1)

60-60: LGTM! Improved log readability with thread names.

Replacing numeric thread IDs with thread names makes logs more intuitive and easier to trace, especially in concurrent scenarios.

core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1)

119-135: Appreciate the richer timeout diagnostics.

Dumping the responsible thread plus the key PeerGroup/NioClientManager workers will make future stalls far easier to root-cause. Nicely scoped to the timeout path.

core/src/main/java/org/bitcoinj/core/PeerGroup.java (2)

2110-2138: LGTM!

The setMaxStalls() and getMaxStalls() methods are properly synchronized and provide a clean API for configuring stall behavior. The ability to disable stall disconnections by setting maxStalls to 0 is a useful feature.


2087-2087: LGTM!

The maxStalls field is properly declared with the @GuardedBy("lock") annotation and a reasonable default value of 3.

Copy link

@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: 2

🧹 Nitpick comments (5)
core/src/main/java/org/bitcoinj/core/listeners/TimeoutError.java (1)

1-6: TimeoutError enum looks good; maybe add brief docs for future clarity

Enum values and naming are clear and match intended use; no functional issues spotted. Consider a short Javadoc on the enum and each constant to lock in semantics as more timeout types get added later.

core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java (1)

1-7: TimeoutErrorListener API is minimal and appropriate

The interface shape (error + PeerAddress) matches how it’s used in PeerSocketHandler/PeerGroup and is easy to implement. If you want, you could later add Javadoc describing threading context and typical callers, but it's not required for correctness.

core/src/main/java/org/bitcoinj/core/PeerGroup.java (3)

163-164: TimeoutError listener wiring is mostly solid; fix docs and consider cleanup of commented code

The overall timeout listener design is consistent with other PeerGroup listeners and looks correct:

  • timeoutErrorListeners uses CopyOnWriteArrayList<ListenerRegistration<TimeoutErrorListener>>, matching patterns used elsewhere.
  • addTimeoutErrorListener propagates the listener to both connected and pending peers, so late registrations see existing peers.
  • connectTo(...) adds all existing timeout listeners to new peers before connection, so timeouts during connect can still be reported.
  • handlePeerDeath(...) correctly removes timeout listeners from a dying peer, keeping peer‑side state clean.
  • queueTimeoutErrorListeners(...) dispatches on the provided Executor without additional locking, consistent with other queue* methods.

A few small issues worth addressing:

  1. Misleading Javadoc for addTimeoutErrorListener
    The comment references OnTransactionBroadcastListener, which is unrelated and will confuse users of this new API.

  2. Left‑over commented‑out code in handleNewPeer
    The timeout listener propagation there is commented:

    //for (ListenerRegistration<TimeoutErrorListener> registration : timeoutErrorListeners)
    //    peer.addTimeoutErrorListener(registration.executor, registration.listener);

    Given you already attach listeners in connectTo(...) and in addTimeoutErrorListener(...) for existing peers, this is redundant and can safely be removed rather than kept commented. Leaving it commented makes it unclear which path is authoritative and risks double‑registration if someone re‑enables it later.

Consider:

-    /** See {@link Peer#addOnTransactionBroadcastListener(OnTransactionBroadcastListener)} */
+    /** Registers listeners that will be notified when any peer in this group reports a timeout. */
     public void addTimeoutErrorListener(Executor executor, TimeoutErrorListener listener) {
         ...
     }
@@
-            //for (ListenerRegistration<TimeoutErrorListener> registration : timeoutErrorListeners)
-            //    peer.addTimeoutErrorListener(registration.executor, registration.listener);
+            // TimeoutErrorListeners are already attached in connectTo(...) and via addTimeoutErrorListener(...)
+            // when listeners are added after peers exist.

Functionally you’re in good shape; this is mainly API clarity and avoiding future confusion.

Also applies to: 915-922, 1035-1043, 1727-1728, 1905-1907, 2092-2093, 2459-2463


2112-2115: Stall configuration via maxStalls works but mutating the config field in-place may surprise callers

The new maxStalls field and setMaxStalls/getMaxStalls methods are correctly guarded by lock and are used safely inside ChainDownloadSpeedCalculator.calculate():

  • Access patterns respect the @GuardedBy("lock") contract.
  • Using getMaxStalls() inside calculate() avoids unsynchronized reads.
  • Decrementing PeerGroup.this.maxStalls under the lock ensures consistent updates, and the single-threaded scheduled executor prevents concurrent calculate() executions.

One behavioral nuance:

  • maxStalls acts as both configuration (“how many stalls are allowed”) and remaining budget (gets decremented each time a stall is detected). External code calling getMaxStalls() after a few stalls will see a reduced number, which may not be obvious from the setter’s Javadoc.

If you want to keep configuration and runtime state separate (to avoid surprising API users), you could maintain:

  • a constant configuredMaxStalls set by setMaxStalls(...), and
  • a private remainingStalls managed only by ChainDownloadSpeedCalculator.

This would let getMaxStalls() report the configured policy while still enforcing a decreasing internal limit.

Also, it may be worth adding a simple guard in setMaxStalls to reject negative values:

public void setMaxStalls(int maxStalls) {
    checkArgument(maxStalls >= 0, "maxStalls must be >= 0");
    ...
}

As implemented, correctness is fine; this is mainly about API semantics and future maintainability.

Also applies to: 2137-2165, 2177-2178, 2261-2278, 2291-2292


1299-1352: Reflection into Guava internals is unsupported and fragile—refactor to capture the delegate directly

The extra logging in stopAsync() and the new forceStop(int waitMsBeforeShutdown) method materially improve debuggability for stuck sync/shutdown scenarios:

  • Logging executor shutdown/termination state and queued tasks is useful when diagnosing hangs.
  • forceStop gives callers a bounded wait followed by shutdownNow() and logs remaining jobs.

However, the reflection approach is problematic:

The delegate field is an undocumented implementation detail. Guava's MoreExecutors.listeningDecorator returns implementations that wrap a delegate via a private nested class, but the public javadoc and API do not promise or expose any accessible delegate field. Reflection on private nested class fields is unsupported and fragile across Guava versions.

Recommended fix: Instead of reflecting into Guava's internals, capture and store the underlying ScheduledThreadPoolExecutor in a private field when you call MoreExecutors.listeningDecorator() in createPrivateExecutor(). This avoids brittle reflection and gives you direct access to executor metrics (queue size, active threads, etc.) without depending on Guava's implementation details.

Regarding forceStop: consider documenting that it should not be used concurrently with stop() and that it can interrupt in-flight tasks (chain download, peer handling).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9fc4de and 233c2ed.

📒 Files selected for processing (4)
  • core/src/main/java/org/bitcoinj/core/PeerGroup.java (14 hunks)
  • core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (5 hunks)
  • core/src/main/java/org/bitcoinj/core/listeners/TimeoutError.java (1 hunks)
  • core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (2)
core/src/main/java/org/bitcoinj/net/NioClient.java (1)
  • NioClient (31-126)
core/src/main/java/org/bitcoinj/net/NioClientManager.java (1)
  • NioClientManager (36-196)
core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java (1)
core/src/main/java/org/bitcoinj/core/PeerAddress.java (1)
  • PeerAddress (37-223)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
🔇 Additional comments (2)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1)

21-29: Timeout listener plumbing and data structures are appropriate

Using CopyOnWriteArrayList<ListenerRegistration<TimeoutErrorListener>> for timeoutErrorListeners matches how other listener sets are handled and is thread-safe for concurrent add/remove/dispatch. Imports and field placement integrate cleanly with existing code.

Also applies to: 41-43, 67-68

core/src/main/java/org/bitcoinj/core/PeerGroup.java (1)

2459-2463: Group-level timeout event dispatch mirrors other listeners and looks correct

queueTimeoutErrorListeners(TimeoutError error, PeerAddress peerAddress) follows the same pattern as other queue* methods in this class: it iterates the timeoutErrorListeners COW list and executes each listener on its configured Executor. This is thread-safe and aligns with expectations for the rest of the listener API.

Copy link

@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: 0

♻️ Duplicate comments (2)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (2)

146-185: Blockstore timeout detection still duplicates stack scans and only drives the boolean off peekByteArray

The structure here still has the two main problems previously flagged: it calls Thread.getAllStackTraces() twice and only returns true for peekByteArray, while mentioning SPVBlockStore in comments/logs and ignoring DirectByteBuffer.get. This means:

  • You pay the cost of walking all stacks twice on every timeout.
  • A timeout stuck in SPVBlockStore.get or DirectByteBuffer.get will be logged in the first pass (if you ever add those checks back) but won’t influence the boolean result that drives TimeoutError.BLOCKSTORE_MEMORY_ACCESS.

A single pass over Thread.getAllStackTraces() that both logs and classifies, and that checks all relevant native frames (peekByteArray, SPVBlockStore.get, DirectByteBuffer.get), keeps behavior consistent and cheaper.

You can refactor along these lines (imports omitted here; you’d also need import java.util.Map;):

-    private boolean checkForBlockStoreTimeout() {
-        Thread.getAllStackTraces().forEach((thread, stackTrace) -> {
-            String threadName = thread.getName();
-            if (threadName.contains("PeerGroup Thread") || threadName.contains("NioClientManager")) {
-                log.warn("Stack trace for thread '{}' (State: {}):", threadName, thread.getState());
-
-                boolean foundBlockingCall = false;
-                for (StackTraceElement element : stackTrace) {
-                    log.warn("  at {}", element);
-
-                    // Check if this thread is stuck in native peekByteArray or SPVBlockStore operations
-                    String elementStr = element.toString();
-                    if (elementStr.contains("peekByteArray")) {
-                        foundBlockingCall = true;
-                    }
-                }
-
-                if (foundBlockingCall) {
-                    log.error("CRITICAL: Thread '{}' is stuck in native I/O operation (peekByteArray/SPVBlockStore)", threadName);
-                }
-            }
-        });
-
-        // Check all threads for blocking SPVBlockStore calls
-        for (Thread thread : Thread.getAllStackTraces().keySet()) {
-            for (StackTraceElement element : thread.getStackTrace()) {
-                String elementStr = element.toString();
-                if (elementStr.contains("peekByteArray")) {
-                    log.error("CRITICAL: Detected SPVBlockStore timeout - native I/O freeze detected in thread: {}", thread.getName());
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
+    private boolean checkForBlockStoreTimeout() {
+        Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+        boolean hasBlockStoreTimeout = false;
+
+        for (Map.Entry<Thread, StackTraceElement[]> entry : allStacks.entrySet()) {
+            Thread thread = entry.getKey();
+            StackTraceElement[] stackTrace = entry.getValue();
+            String threadName = thread.getName();
+            if (threadName.contains("PeerGroup Thread") || threadName.contains("NioClientManager")) {
+                log.warn("Stack trace for thread '{}' (State: {}):", threadName, thread.getState());
+
+                boolean foundBlockingCall = false;
+                for (StackTraceElement element : stackTrace) {
+                    log.warn("  at {}", element);
+
+                    String elementStr = element.toString();
+                    if (elementStr.contains("peekByteArray")
+                            || elementStr.contains("SPVBlockStore.get")
+                            || elementStr.contains("DirectByteBuffer.get")) {
+                        foundBlockingCall = true;
+                    }
+                }
+
+                if (foundBlockingCall) {
+                    log.error("CRITICAL: Thread '{}' is stuck in native I/O operation (peekByteArray/SPVBlockStore)", threadName);
+                    hasBlockStoreTimeout = true;
+                }
+            }
+        }
+
+        if (hasBlockStoreTimeout) {
+            log.error("CRITICAL: Detected SPVBlockStore timeout - native I/O freeze detected");
+        }
+        return hasBlockStoreTimeout;
+    }

This removes the second scan, makes the boolean result match what you log, and properly classifies all three native I/O hotspots as BLOCKSTORE_MEMORY_ACCESS.


314-323: Fix timeout listener Javadoc and removal logging to match behavior

Same two nits as previously noted still apply here:

  1. Javadoc for addTimeoutErrorListener is copy‑pasted from the pre‑message listener API and does not describe timeout behavior.
  2. removeTimeoutErrorListener logs timeoutErrorListeners.size() - 1 before removal and regardless of whether the listener was actually present, so the “new size” can be wrong.

Consider tightening this as follows:

-    /** Registers a listener that is called immediately before a message is received */
+    /** Registers a listener that will be notified when this peer experiences a socket timeout. */
     public void addTimeoutErrorListener(Executor executor, TimeoutErrorListener listener) {
         timeoutErrorListeners.add(new ListenerRegistration<>(listener, executor));
         log.info("timeout error listener added: {}, {}", timeoutErrorListeners.size(), getAddress());
     }
 
     public boolean removeTimeoutErrorListener(TimeoutErrorListener listener) {
-        log.info("timeout error listener removed: {}, {}", timeoutErrorListeners.size() - 1, getAddress());
-        return ListenerRegistration.removeFromList(listener, timeoutErrorListeners);
+        boolean removed = ListenerRegistration.removeFromList(listener, timeoutErrorListeners);
+        if (removed) {
+            log.info("timeout error listener removed: {}, {}", timeoutErrorListeners.size(), getAddress());
+        } else {
+            log.info("timeout error listener not found for removal: {}", getAddress());
+        }
+        return removed;
     }

The rest of the listener infrastructure (use of CopyOnWriteArrayList and ListenerRegistration, and asynchronous dispatch in queueTimeoutErrorListeners) looks good.

🧹 Nitpick comments (3)
core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java (1)

861-890: Null-guard in getHashModifier is correct; document nullable contract and verify callsites

The new workBlock == null check cleanly avoids NPEs when the ancestor isn’t in the store yet during sync, which aligns with the added guards in QuorumState.computeQuorumMembers.

Since getHashModifier now explicitly returns null in this situation, consider:

  • Marking getHashModifier as @Nullable (and updating any Javadoc) to make the contract clear.
  • Quickly scanning other callsites beyond QuorumState.computeQuorumMembers to ensure they either handle null or are updated accordingly.
core/src/main/java/org/bitcoinj/evolution/QuorumState.java (1)

195-207: Defensive null handling in computeQuorumMembers looks good; confirm caller expectations and consider small refinements

The new early returns when workBlock or the hash modifier are null sensibly prevent crashes when the ancestor block or CL-based modifier is unavailable during sync.

A couple of follow-ups to consider:

  • computeQuorumMembers / getAllQuorumMembers can now return null in more situations (on top of the existing allMns == null path). Please confirm all callers treat null as “cannot compute quorum” and don’t blindly iterate the result.
  • For clarity, you might document the distinction between:
    • “No quorum at this height” → empty list ([]), and
    • “Quorum cannot be computed (state incomplete)” → null.
  • Minor: workBlock is computed here, and getHashModifier internally recomputes the same ancestor; if this is on a hot path, consider passing workBlock down or refactoring to avoid the redundant lookup.
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1)

125-143: Remove unused currentThread variable in timeoutOccurred

Thread currentThread = Thread.currentThread(); is never used and will trigger IDE/compiler warnings. You can safely drop it without changing behavior:

-    protected void timeoutOccurred() {
-        Thread currentThread = Thread.currentThread();
+    protected void timeoutOccurred() {
         log.info("{}: Timed out", getAddress());
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 233c2ed and 5e4321d.

📒 Files selected for processing (5)
  • core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (5 hunks)
  • core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/QuorumState.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListDiff.java (1 hunks)
  • core/src/main/java/org/bitcoinj/quorums/SimplifiedQuorumList.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • core/src/main/java/org/bitcoinj/quorums/SimplifiedQuorumList.java
🧰 Additional context used
🧬 Code graph analysis (1)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (2)
core/src/main/java/org/bitcoinj/net/NioClient.java (1)
  • NioClient (31-126)
core/src/main/java/org/bitcoinj/net/NioClientManager.java (1)
  • NioClientManager (36-196)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
🔇 Additional comments (3)
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListDiff.java (1)

207-209: Multi-line toString() output may affect log consumers

The added %n %s -> %s makes toString() return a multi-line string. That’s likely fine and improves readability, but be aware it can impact any tooling or tests that expect single-line log entries or parse toString() output line-by-line. If any such consumers exist, consider keeping it single-line or adding a dedicated, more verbose debug method instead.

core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (2)

21-42: Timeout listener imports and threading primitives look appropriate

Using TimeoutError/TimeoutErrorListener plus ListenerRegistration with CopyOnWriteArrayList and Executor matches the existing listener patterns and gives you safe concurrent iteration and dispatch. No issues here.


325-335: Timeout listener dispatch pattern looks solid

queueTimeoutErrorListeners correctly logs the queueing event and uses each listener’s configured Executor to invoke onTimeout(error, peer) without holding any locks or touching socket state. This matches the listener patterns elsewhere in the codebase and should be safe under concurrent add/remove operations thanks to the CopyOnWriteArrayList.

Copy link

@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: 0

♻️ Duplicate comments (2)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (2)

124-144: Consolidate stack trace scan and align blockstore timeout detection with intent

checkForBlockStoreTimeout() currently:

  • Calls Thread.getAllStackTraces() twice and walks stacks twice, which is unnecessary overhead on every timeout.
  • Only checks for "peekByteArray" even though comments/logs mention “peekByteArray or SPVBlockStore operations”, so classification and log text can drift from actual detection.
  • Uses one pass mainly for logging and a second pass to drive the boolean return, instead of a single pass that both logs and determines true/false.

You can simplify and make behavior match the comments by collecting stacks once, scanning them once, and using the same predicate both for logging and for the boolean result. For example:

-    private boolean checkForBlockStoreTimeout() {
-        Thread.getAllStackTraces().forEach((thread, stackTrace) -> {
-            String threadName = thread.getName();
-            if (threadName.contains("PeerGroup Thread") || threadName.contains("NioClientManager")) {
-                log.warn("Stack trace for thread '{}' (State: {}):", threadName, thread.getState());
-
-                boolean foundBlockingCall = false;
-                for (StackTraceElement element : stackTrace) {
-                    log.warn("  at {}", element);
-
-                    // Check if this thread is stuck in native peekByteArray or SPVBlockStore operations
-                    String elementStr = element.toString();
-                    if (elementStr.contains("peekByteArray")) {
-                        foundBlockingCall = true;
-                    }
-                }
-
-                if (foundBlockingCall) {
-                    log.error("CRITICAL: Thread '{}' is stuck in native I/O operation (peekByteArray/SPVBlockStore)", threadName);
-                }
-            }
-        });
-
-        // Check all threads for blocking SPVBlockStore calls
-        for (Thread thread : Thread.getAllStackTraces().keySet()) {
-            for (StackTraceElement element : thread.getStackTrace()) {
-                String elementStr = element.toString();
-                if (elementStr.contains("peekByteArray")) {
-                    log.error("CRITICAL: Detected SPVBlockStore timeout - native I/O freeze detected in thread: {}", thread.getName());
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
+    private boolean checkForBlockStoreTimeout() {
+        Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces();
+        boolean hasBlockStoreTimeout = false;
+
+        for (Map.Entry<Thread, StackTraceElement[]> entry : allStacks.entrySet()) {
+            Thread thread = entry.getKey();
+            StackTraceElement[] stackTrace = entry.getValue();
+            String threadName = thread.getName();
+
+            boolean interestingThread = threadName.contains("PeerGroup Thread") || threadName.contains("NioClientManager");
+            if (interestingThread) {
+                log.warn("Stack trace for thread '{}' (State: {}):", threadName, thread.getState());
+            }
+
+            boolean foundBlockingCall = false;
+            for (StackTraceElement element : stackTrace) {
+                if (interestingThread) {
+                    log.warn("  at {}", element);
+                }
+                String elementStr = element.toString();
+                if (elementStr.contains("peekByteArray")
+                        || elementStr.contains("SPVBlockStore.get")
+                        || elementStr.contains("DirectByteBuffer.get")) {
+                    foundBlockingCall = true;
+                }
+            }
+
+            if (foundBlockingCall) {
+                hasBlockStoreTimeout = true;
+                log.error("CRITICAL: Detected native I/O freeze (peekByteArray/SPVBlockStore/DirectByteBuffer) in thread: {}", threadName);
+            }
+        }
+
+        return hasBlockStoreTimeout;
+    }

You’ll also need to add import java.util.Map; at the top.

This keeps the timeout classification logic in one place, avoids the duplicate global stack snapshot, and ensures the return value matches the conditions mentioned in the comments/log messages.

Also applies to: 146-185


314-332: Fix copy‑pasted Javadoc for timeout error listeners

The Javadoc on addTimeoutErrorListener still says “called immediately before a message is received”, which is misleading for a timeout error listener API.

Consider updating it to describe the actual behavior, e.g.:

-    /** Registers a listener that is called immediately before a message is received */
+    /** Registers a listener that will be notified when this peer experiences a socket timeout. */

This makes the API self‑documenting for anyone discovering it from PeerSocketHandler.

🧹 Nitpick comments (1)
core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (1)

196-220: Consider using DEBUG level for detailed timing logs.

The detailed timing breakdown is logged at INFO level, which will generate a log entry on every wallet write. This might be verbose in production environments. Consider using DEBUG level for these performance diagnostics while keeping high-level timing at INFO if needed.

Apply this diff to adjust the log level:

-        log.info("writeWallet timing: {}ms total ({}ms/{}% serialization, {}ms/{}% I/O) buffer={}KB", 
+        log.debug("writeWallet timing: {}ms total ({}ms/{}% serialization, {}ms/{}% I/O) buffer={}KB", 
             totalMs, serializationMs, serializationPercent, ioMs, ioPercent, bufferSize/1024);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4321d and d679251.

📒 Files selected for processing (2)
  • core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (5 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
🔇 Additional comments (3)
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1)

21-23: Timeout error listener wiring and types look consistent

The new imports and the CopyOnWriteArrayList<ListenerRegistration<TimeoutErrorListener>> field are appropriate for the listener pattern used elsewhere in bitcoinj and are thread‑safe for concurrent registration and dispatch.

Also applies to: 28-28, 41-42, 67-68

core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (2)

148-149: Good fix for backward compatibility.

This correctly addresses the past review comment by disabling adaptive sizing when an explicit buffer size is set, preserving the legacy contract for existing integrators.


164-189: The Wallet.getKeyChainGroupSize() method exists and is properly defined in the Wallet class as a public method (line 853 of core/src/main/java/org/bitcoinj/wallet/Wallet.java). The method is documented and used throughout the codebase, so the call in the calculateOptimalBufferSize method will execute without runtime errors.

@HashEngineering HashEngineering force-pushed the fix/sync-issues-coinjoin branch from d679251 to 5c79026 Compare November 25, 2025 01:55
Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java (1)

860-890: Null-handling gaps in getHashModifier call sites require critical fixes

The new null guard in getHashModifier correctly prevents NPE during sync, but three of four call sites do not handle the nullable return, creating NPE risks when the ancestor block is unavailable:

  • QuorumRotationState.java:685 (buildNewQuorumQuarterMembers): modifier is passed directly to calculateQuorum() at lines 735 and 737 without null check
  • QuorumRotationState.java:835: modifier is passed directly to calculateQuorum() at lines 841–842 without null check
  • QuorumRotationState.java:913 (getMNUsageBySnapshot): modifier is passed directly to calculateQuorum() at line 927 without null check

Add null checks at these three sites before using modifier, or add @Nullable annotation to the method return type to document the contract clearly.

♻️ Duplicate comments (3)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

2590-2596: Remove dead hardSaveOnNextBlock state and commented conditional in notifyNewBestBlock().

The hardSaveOnNextBlock block here is fully commented out, but the flag is still declared (private boolean hardSaveOnNextBlock = false;) and written in receive() (hardSaveOnNextBlock = true;), leaving unused state and misleading logic.

Within this hunk, drop the commented conditional so the method always coalesces via saveLater():

-            //if (hardSaveOnNextBlock) {
-            //    saveNow();
-            //    hardSaveOnNextBlock = false;
-            //} else {
                 // Coalesce writes to avoid throttling on disk access when catching up with the chain.
                 saveLater();
-            //}

Additionally (outside this hunk), remove:

  • The field private boolean hardSaveOnNextBlock = false; (around Line 2329).
  • The assignment hardSaveOnNextBlock = true; at the end of receive(...) (around Line 2481).

After this cleanup, all persistence is consistently driven by saveLater() in this path.


853-870: Fix lock ordering and synchronize access to keyChainExtensions in getKeyChainGroupSize().

This method holds only keyChainGroupLock but iterates keyChainExtensions, which is mutated under the wallet lock (e.g. in addExtension/deserializeExtension). That violates the documented lock ordering “lock > keyChainGroupLock” (Lines 134–137) and risks data races/ConcurrentModificationException.

Acquire lock first, then keyChainGroupLock, and drop the unnecessary AtomicInteger:

     public int getKeyChainGroupSize() {
-        keyChainGroupLock.lock();
-        try {
-            AtomicInteger walletKeys = new AtomicInteger(keyChainGroup.numKeys());
-
-            if (receivingFromFriendsGroup != null) {
-                walletKeys.addAndGet(receivingFromFriendsGroup.numKeys());
-            }
-
-            if (sendingToFriendsGroup != null) {
-                walletKeys.addAndGet(sendingToFriendsGroup.numKeys());
-            }
-
-            keyChainExtensions.values().forEach(keyChainGroupExtension ->
-                walletKeys.addAndGet(keyChainGroupExtension.numKeys())
-            );
-            return walletKeys.get();
-        } finally {
-            keyChainGroupLock.unlock();
-        }
+        lock.lock();
+        try {
+            keyChainGroupLock.lock();
+            try {
+                int walletKeys = keyChainGroup.numKeys();
+
+                if (receivingFromFriendsGroup != null) {
+                    walletKeys += receivingFromFriendsGroup.numKeys();
+                }
+
+                if (sendingToFriendsGroup != null) {
+                    walletKeys += sendingToFriendsGroup.numKeys();
+                }
+
+                for (KeyChainGroupExtension ext : keyChainExtensions.values()) {
+                    walletKeys += ext.numKeys();
+                }
+                return walletKeys;
+            } finally {
+                keyChainGroupLock.unlock();
+            }
+        } finally {
+            lock.unlock();
+        }
     }
core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (1)

124-144: Timeout cause detection still does duplicate stack scans and only matches peekByteArray

  • checkForBlockStoreTimeout() calls Thread.getAllStackTraces() twice and re‑walks all stacks, which is unnecessarily expensive and noisy, especially when many threads exist.
  • Despite comments/logs mentioning “SPVBlockStore operations”, the detection only checks for "peekByteArray"; the error log message ("(peekByteArray/SPVBlockStore)") is therefore misleading.
  • The local currentThread in timeoutOccurred() is unused.

Consider:

  • Calling Thread.getAllStackTraces() once, logging and computing the boolean in a single pass, and
  • Either broadening the match (e.g. include SPVBlockStore/other native I/O indicators as intended) or updating comments/log strings to reflect that you’re only keying off peekByteArray,
  • Dropping the unused currentThread variable.

This keeps behavior while reducing overhead and making the logs match the actual detection.

Also applies to: 146-185

🧹 Nitpick comments (8)
core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (1)

95-220: Adaptive buffer sizing + explicit override behavior looks sound

The combination of:

  • useAdaptiveBufferSizing defaulting to true,
  • setWalletWriteBufferSize(...) automatically disabling adaptivity, and
  • setUseAdaptiveBufferSizing(boolean) to re‑enable it

preserves legacy “explicit buffer wins” semantics while introducing a safe, bounded 8/16/32/64 KB heuristic via calculateOptimalBufferSize(...). The updated writeWallet path correctly separates serialization and I/O timing, uses the computed buffer with CodedOutputStream, and maintains the existing exception contract.

One minor consideration: the new log.info("writeWallet timing: ...") on every save could be noisy for applications that persist frequently; consider downgrading to debug or gating behind log.isDebugEnabled() if log volume becomes an issue.

-        log.info("writeWallet timing: {}ms total ({}ms/{}% serialization, {}ms/{}% I/O) buffer={}KB", 
-            totalMs, serializationMs, serializationPercent, ioMs, ioPercent, bufferSize/1024);
+        if (log.isDebugEnabled()) {
+            log.debug("writeWallet timing: {}ms total ({}ms/{}% serialization, {}ms/{}% I/O) buffer={}KB",
+                    totalMs, serializationMs, serializationPercent, ioMs, ioPercent, bufferSize / 1024);
+        }
core/src/main/java/org/bitcoinj/store/FlatDB.java (2)

29-135: Clarify interaction between setIOBufferSize and adaptive sizing

The adaptive calculateOptimalBufferSize(long dataSize) logic is reasonable and the 4/16/64/128 KB tiers are conservative. However, with useAdaptiveBufferSizing defaulting to true, a caller that invokes setIOBufferSize(...) alone will not actually get that size at runtime unless they also call setUseAdaptiveBufferSizing(false); the adaptive path will still pick its own tier.

For consistency with WalletProtobufSerializer.setWalletWriteBufferSize(...) and to make the API more intuitive, consider automatically disabling adaptive sizing when an explicit buffer is set:

public void setIOBufferSize(int bufferSize) {
-    this.ioBufferSize = bufferSize;
+    this.ioBufferSize = bufferSize;
+    this.useAdaptiveBufferSizing = false;
}

Callers that want adaptivity back can still re‑enable it with setUseAdaptiveBufferSizing(true).


163-224: Buffered I/O integration preserves behavior and improves performance

Switching both write and read to use BufferedOutputStream/BufferedInputStream with the new calculateOptimalBufferSize(...) keeps the existing on‑disk format intact while reducing syscall overhead on larger cache files. The try‑with‑resources blocks also tighten resource handling.

If you touch this code again, you might consider using DataInputStream.readFully(...) (or manually looping on read) instead of assuming a single read fills vchData and hashIn, but that limitation was already present before the buffering change.

core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (3)

46-119: Verbose setup and log formatting in test harness

Switching to BriefLogFormatter.initVerbose() and adding the custom info/error/formatMessage helpers makes the perf tests much easier to inspect, and all usage of {} / {:.xf} placeholders is handled safely for the current calls. Just be aware this greatly increases console output when running the test suite; if CI runtimes or log volume become an issue, consider downgrading some of these to use the standard SLF4J logger or guarding the custom logging behind a verbosity flag.


140-332: Comprehensive buffer-size perf benchmark; consider treating as a “slow” test

walletSavePerformanceTest provides a very detailed comparison of fixed buffer sizes vs adaptive sizing (including warmups, multiple runs, relative-speed bars, and on-disk file tests). It’s excellent for tuning and regression analysis, but it runs a large number of serializations and emits a lot of log output.

If this starts to impact CI time or noise, it might be worth:

  • annotating it or moving it to a dedicated perf suite (e.g., using a separate JUnit category), or
  • reducing the number of runs / buffer sizes when not in a dedicated performance run.

Functionally, the logic is sound and does not affect production code paths.


417-570: Robust save/load consistency and caching test

walletConsistencyAndCachingPerformanceTest plus compareWallets gives strong coverage for:

  • binary stability across repeated save/load cycles (including memos, exchange rates, cached values, and coinjoin types), and
  • performance gains from protobuf caching on subsequent saves.

The transaction comparison via TxID map avoids ordering issues from the underlying sets, and the assertions on counts/balances are appropriate. Similar to the other perf test, this is relatively heavy and verbose but technically solid; you may eventually want to mark it as a “slow” or perf‑focused test if suite duration grows.

core/src/main/java/org/bitcoinj/core/PeerGroup.java (2)

1299-1351: Reflection on Guava’s ListeningScheduledExecutorService is brittle; prefer an explicit delegate reference

The shutdown diagnostics in stopAsync() are useful, but relying on:

java.lang.reflect.Field delegateField = executor.getClass().getDeclaredField("delegate");

to reach a ScheduledThreadPoolExecutor inside Guava’s listening decorator ties you to a private field name and implementation detail. A Guava upgrade or different ListeningScheduledExecutorService implementation would make this path silently degrade (aside from the logged message).

Consider instead holding a direct reference to the underlying ScheduledThreadPoolExecutor when you construct the executor (e.g., store it in a separate field) and logging against that, avoiding reflection entirely. The existing try/catch makes this non‑fatal today, but the code will be more robust and maintainable with an explicit delegate.


1393-1437: forceStop works but job descriptions may rarely match actual queued tasks

forceStop(int waitMsBeforeShutdown) correctly:

  • Triggers stopAsync() when still running,
  • Waits up to waitMsBeforeShutdown,
  • Falls back to shutdownNow() and logs remaining jobs.

However, getJobDescription(Runnable job) relies on job == triggerConnectionsJob and instanceof ChainDownloadSpeedCalculator. In practice, the underlying ScheduledThreadPoolExecutor queues its own RunnableScheduledFuture wrappers, so these identity/type checks are unlikely to ever match, and you’ll usually log “Unknown task”.

If the extra classification is important, you may want to derive descriptions from job.toString() patterns only, or avoid over‑promising detail in the log messages (“Remaining job” without a semantic label is still useful). Otherwise the method is harmless but not very informative.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d679251 and 5c79026.

📒 Files selected for processing (19)
  • core/src/main/java/org/bitcoinj/core/PeerGroup.java (14 hunks)
  • core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java (5 hunks)
  • core/src/main/java/org/bitcoinj/core/listeners/TimeoutError.java (1 hunks)
  • core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/AbstractQuorumState.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/QuorumState.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListDiff.java (1 hunks)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1 hunks)
  • core/src/main/java/org/bitcoinj/quorums/SimplifiedQuorumList.java (1 hunks)
  • core/src/main/java/org/bitcoinj/store/FlatDB.java (5 hunks)
  • core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java (2 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletFiles.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java (3 hunks)
  • core/src/test/java/org/bitcoinj/core/PeerGroupTest.java (1 hunks)
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (4 hunks)
  • examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • core/src/main/java/org/bitcoinj/utils/BriefLogFormatter.java
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java
  • core/src/main/java/org/bitcoinj/quorums/SimplifiedQuorumList.java
  • core/src/test/java/org/bitcoinj/core/PeerGroupTest.java
  • examples/src/main/java/org/bitcoinj/examples/RestoreFromSeedThenDump.java
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListDiff.java
  • core/src/main/java/org/bitcoinj/core/listeners/TimeoutErrorListener.java
  • core/src/main/java/org/bitcoinj/wallet/WalletFiles.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-18T22:12:09.613Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 286
File: core/src/main/java/org/bitcoinj/wallet/Wallet.java:6371-6373
Timestamp: 2025-09-18T22:12:09.613Z
Learning: In bitcoinj's Wallet.java, methods returning List<ECKey> can safely return new LinkedList<>(detkeys) where detkeys is List<DeterministicKey>, because DeterministicKey extends ECKey and Java's type inference handles this covariance correctly during list construction.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
  • core/src/main/java/org/bitcoinj/wallet/WalletEx.java
  • core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java
🧬 Code graph analysis (2)
core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1)
core/src/main/java/org/bitcoinj/core/TransactionConfidence.java (1)
  • TransactionConfidence (66-714)
core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1)
core/src/main/java/org/bitcoinj/core/Sha256Hash.java (1)
  • Sha256Hash (38-315)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
🔇 Additional comments (5)
core/src/main/java/org/bitcoinj/wallet/WalletEx.java (1)

616-633: Thread-safe guard around myUnspents iteration looks correct

Taking the wallet lock for countInputsWithAmount aligns this method with the rest of WalletEx’s concurrency model, avoiding concurrent modification of myUnspents. Logic and behavior are otherwise unchanged.

core/src/main/java/org/bitcoinj/core/listeners/TimeoutError.java (1)

1-6: Timeout error enum is simple and extensible

Defining TimeoutError with BLOCKSTORE_MEMORY_ACCESS and UNKNOWN provides a clear, type-safe channel for timeout diagnostics and leaves room to extend with more specific causes later.

core/src/main/java/org/bitcoinj/evolution/QuorumState.java (1)

183-211: Null-safety improvements in computeQuorumMembers verified

The new guards for workBlock == null and modifier == null correctly turn previously fatal conditions into logged early-returns. The only call site (SimplifiedQuorumList.java:430) already handles null results defensively with an explicit null-check and warning log. The nullable return contract is preserved and properly maintained throughout the call chain.

core/src/main/java/org/bitcoinj/wallet/CoinJoinExtension.java (1)

130-201: CoinJoin extension serialization enhancements validated

The reworked serializeWalletExtension method properly:

  • cleanly separates and times key-chain, outpoint-rounds cache, salt handling, and protobuf I/O
  • persists WalletEx.mapOutpointRoundsCache into the OutpointRoundsCache proto while the wallet lock is held (via Wallet.saveToFileStreamwriteWalletwalletToProtopopulateExtensions)
  • uses a sized ByteArrayOutputStream plus CodedOutputStream with max(64KB, serializedSize/4) for efficient writes

while deserializeWalletExtension restores the cache and coinjoin salt. The wallet's main lock is held throughout the serialization chain, preventing any race with cache updates. This design aligns with the principle that mapOutpointRoundsCache persists across normal operations and is only cleared on reorg/reset.

Optional: Consider gating the detailed timing log at info level behind log.isDebugEnabled() to reduce verbosity in production saves, though the current approach is acceptable for diagnostic purposes.

core/src/main/java/org/bitcoinj/core/PeerGroup.java (1)

2112-2115: Configurable maxStalls and updated stall handling look coherent

The new maxStalls field, setMaxStalls(int)/getMaxStalls(), and the integration into ChainDownloadSpeedCalculator.calculate() make the stall behavior tunable and behave as described:

  • maxStalls <= 0 disables stall disconnections (logging only).
  • Each time a stall is detected, maxStalls is decremented under the outer lock, and when it reaches zero, you stop disconnecting peers and log that the network appears slower than the threshold.
  • Using getMaxStalls() for the read and a locked decrement keeps this reasonably thread‑safe given how the calculator runs.

This matches the Javadoc semantics and doesn’t introduce obvious race or off‑by‑one issues.

Also applies to: 2137-2165, 2219-2261, 2270-2291

Copy link

@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: 2

♻️ Duplicate comments (1)
core/src/test/java/org/bitcoinj/core/PeerGroupTest.java (1)

903-913: Flaky timing assertion for 50ms wait due to low timer resolution.

As noted in the previous review, System.currentTimeMillis() has platform-specific variations, "with Windows typically providing 15.625ms granularity while Mac and Linux systems achieve ~1ms precision." For a 50ms wait, this can cause the assertion >= 50 to occasionally fail.

The past review suggested using System.nanoTime() with TimeUnit.MILLISECONDS.toNanos() for reliable sub-100ms timing assertions.

🧹 Nitpick comments (1)
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (1)

539-542: Prefer assertNotNull over assertEquals(..., true, tx2 != null).

The current pattern is verbose and less idiomatic. assertNotNull provides clearer semantics and better failure messages.

-            // Verify the transaction exists in wallet2
-            assertEquals("Transaction " + tx1.getTxId() + " should exist in wallet #" + wallet2Number,
-                true, tx2 != null);
+            // Verify the transaction exists in wallet2
+            assertNotNull("Transaction " + tx1.getTxId() + " should exist in wallet #" + wallet2Number, tx2);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c79026 and d7d8b42.

📒 Files selected for processing (2)
  • core/src/test/java/org/bitcoinj/core/PeerGroupTest.java (1 hunks)
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
🔇 Additional comments (4)
core/src/test/java/org/bitcoinj/core/PeerGroupTest.java (1)

888-901: LGTM!

The test correctly validates forceStop behavior when the PeerGroup is running. The 10-second threshold is sufficiently large that System.currentTimeMillis() resolution differences across platforms won't cause flakiness.

core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (3)

47-59: LGTM!

The setup method changes appropriately enable verbose logging for debugging performance tests and load the updated wallet file.


72-76: LGTM!

The expected balance update aligns with loading a different wallet file (coinjoin-cache.wallet) in the setup method.


99-107: LGTM!

The switch to toString(false, false, true, null) consistently tests the non-CoinJoin variant for both initial and subsequent calls.

Copy link

@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: 1

🧹 Nitpick comments (3)
core/src/main/java/org/bitcoinj/core/PeerGroup.java (3)

163-164: TimeoutErrorListener wiring looks consistent with existing listener patterns

The new timeoutErrorListeners collection plus addTimeoutErrorListener, removeTimeoutErrorListener, early registration in connectTo(...), cleanup in handlePeerDeath(...), and queueTimeoutErrorListeners(...) all mirror how other listener types are managed (blocks, headers, masternode list, etc.). The lifecycle and thread‑safety via CopyOnWriteArrayList and per‑peer add/remove calls look sound.

If you anticipate external users registering timeout listeners frequently, you might consider adding a convenience overload addTimeoutErrorListener(TimeoutErrorListener listener) that defaults to Threading.USER_THREAD, in line with the other listener APIs, but this is optional and not required for correctness.

Also applies to: 915-921, 1034-1042, 1725-1727, 2089-2090, 2456-2460


1298-1377: Executor shutdown diagnostics are useful but rely on Guava internals; consider holding the delegate directly

The extra logging in stopAsync() and forceStop() (reflection to get the delegate ScheduledThreadPoolExecutor, queue dumps, thread stack traces, and job labeling via getJobDescription) will be valuable when debugging hangs during shutdown. Two improvement points:

  1. The reflection on executor.getClass().getDeclaredField("delegate") reaches into Guava’s private implementation details. Although you safely catch exceptions, this is brittle across Guava upgrades. Since you create the ScheduledThreadPoolExecutor yourself in createPrivateExecutor(), you could store it in a dedicated field (e.g., private final ScheduledThreadPoolExecutor executorDelegate) at creation time and use that directly here, avoiding reflection entirely.
  2. Enumerating all threads and logging stack traces at INFO might be quite noisy in environments with many threads. You may want to guard that part behind a debug flag or log.isDebugEnabled() check so production logs stay cleaner while retaining the tool for investigations.

These are maintainability/observability refinements; the current behavior is functionally safe because failures in the reflective path are contained to logging.

Also applies to: 1392-1436


2109-2112: Configurable maxStalls works, but contract around values could be tightened

The introduction of @GuardedBy("lock") int maxStalls, plus setMaxStalls(int)/getMaxStalls() and the use of this value in ChainDownloadSpeedCalculator.calculate() to decrement and eventually stop stall‑based disconnects, gives a clear and tunable behavior:

  • maxStalls > 0: limited number of stall‑triggered disconnects, with the log message showing remaining stalls.
  • maxStalls <= 0: treated as “stall disconnects disabled” (currentMaxStalls <= 0 path).

Two minor points you may want to refine:

  1. The Javadoc on setMaxStalls says “0 to disable stall disconnections entirely”, but negative values are also effectively treated as disabled. Either clamp negatives to 0 or document that “<= 0 disables stall disconnects” to keep the contract precise.
  2. ChainDownloadSpeedCalculator decrements PeerGroup.this.maxStalls under lock while operating under its own synchronized (this) block. That ordering (first synchronized(this) in calculate, then acquiring PeerGroup.lock) is only one‑way and, as written, doesn’t have any corresponding code that acquires PeerGroup.lock and then synchronizes on the calculator, so there’s no immediate deadlock risk. If you later add such code, preserve this lock ordering.

Overall, the new stall control logic looks correct and should help avoid overly aggressive disconnects on persistently slow networks.

Also applies to: 2134-2162, 2258-2275, 2288-2289

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7d8b42 and dd73c93.

📒 Files selected for processing (3)
  • core/src/main/java/org/bitcoinj/core/PeerGroup.java (13 hunks)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java (4 hunks)
  • core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
📚 Learning: 2025-09-18T22:12:09.613Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 286
File: core/src/main/java/org/bitcoinj/wallet/Wallet.java:6371-6373
Timestamp: 2025-09-18T22:12:09.613Z
Learning: In bitcoinj's Wallet.java, methods returning List<ECKey> can safely return new LinkedList<>(detkeys) where detkeys is List<DeterministicKey>, because DeterministicKey extends ECKey and Java's type inference handles this covariance correctly during list construction.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
  • core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
🔇 Additional comments (2)
core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java (1)

163-163: Add explanatory comment documenting the expected key composition.

The key count of 18 is mathematically correct for the test setup:

  • Lookahead zone: (lookaheadSize + threshold) × 2 chains = (5 + 1) × 2 = 12 keys
  • Hierarchy keys: seed (1) + master (1) + account (1) + parent keys (2) = 5 keys
  • Imported key: 1 key
  • Total: 18 keys

For the UNITTEST network, KeyChainGroup defaults to lookaheadSize=5 (unit tests use reduced settings). The change from 6 to 18 reflects keys now being counted correctly or the lookahead configuration being applied during serialization.

Recommendation: Add a comment to document this calculation:

// Expected: (lookaheadSize=5 + threshold=1) * 2 chains + hierarchy keys (5) + imported key (1) = 18 total
assertEquals(18, walletProto.getKeyCount());

This makes the assertion self-documenting and ensures future maintainers understand why 18 is correct.

core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)

855-870: Reworked getKeyChainGroupSize now respects lock ordering and avoids races.

This implementation acquires lock and then keyChainGroupLock and uses a simple int accumulator instead of AtomicInteger, which aligns with the documented lock ordering and avoids concurrent iteration issues over keyChainExtensions. Just ensure callers do not already hold keyChainGroupLock without lock, to avoid potential lock-order inversions.

Copy link

@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: 0

🧹 Nitpick comments (4)
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (4)

249-253: Potential division by zero if elapsed time is 0 ms.

If originalTime is 0 (possible on very fast systems or due to timing granularity), these divisions will produce Infinity or NaN. Consider adding a guard.

-        double improvementOverOriginal = ((double)(originalTime - adaptiveAvg) / originalTime) * 100;
-        double improvementOverBest = ((double)(bestFixedTime - adaptiveAvg) / bestFixedTime) * 100;
-        
-        info("Adaptive vs Original 4KB: {:.1f}% improvement", improvementOverOriginal);
-        info("Adaptive vs Best Fixed: {:.1f}% improvement", improvementOverBest);
+        if (originalTime > 0) {
+            double improvementOverOriginal = ((double)(originalTime - adaptiveAvg) / originalTime) * 100;
+            info("Adaptive vs Original 4KB: {:.1f}% improvement", improvementOverOriginal);
+        }
+        if (bestFixedTime > 0) {
+            double improvementOverBest = ((double)(bestFixedTime - adaptiveAvg) / bestFixedTime) * 100;
+            info("Adaptive vs Best Fixed: {:.1f}% improvement", improvementOverBest);
+        }

313-314: Same division by zero risk with file I/O timing.

If the file save completes in under 1 ms, this calculation will fail.

-            double fileImprovementPercent = ((double) (originalFileWatch.elapsed().toMillis() - adaptiveFileWatch.elapsed().toMillis()) / originalFileWatch.elapsed().toMillis()) * 100;
-            info("File I/O improvement: {:.1f}%", fileImprovementPercent);
+            long originalFileMs = originalFileWatch.elapsed().toMillis();
+            if (originalFileMs > 0) {
+                double fileImprovementPercent = ((double) (originalFileMs - adaptiveFileWatch.elapsed().toMillis()) / originalFileMs) * 100;
+                info("File I/O improvement: {:.1f}%", fileImprovementPercent);
+            }

539-541: Use assertNotNull for null checks.

assertEquals(true, tx2 != null) is awkward and produces less helpful failure messages than assertNotNull.

-            assertEquals("Transaction " + tx1.getTxId() + " should exist in wallet #" + wallet2Number,
-                true, tx2 != null);
+            assertNotNull("Transaction " + tx1.getTxId() + " should exist in wallet #" + wallet2Number, tx2);

This requires adding import static org.junit.Assert.assertNotNull; or using the fully qualified call.


548-559: Exchange rate comparison logic is correct but could be clearer.

The nested conditionals work correctly, but the else branch assertion message is slightly misleading. Consider simplifying:

-            if (tx1.getExchangeRate() == null) {
-                assertEquals("Transaction " + tx1.getTxId() + " exchange rate should be null in both wallets",
-                    null, tx2.getExchangeRate());
-            } else if (tx2.getExchangeRate() != null) {
-                assertEquals("Transaction " + tx1.getTxId() + " exchange rate should match between wallets",
-                    tx1.getExchangeRate().coin, tx2.getExchangeRate().coin);
-                assertEquals("Transaction " + tx1.getTxId() + " exchange rate fiat should match between wallets",
-                    tx1.getExchangeRate().fiat, tx2.getExchangeRate().fiat);
-            } else {
-                assertEquals("Transaction " + tx1.getTxId() + " exchange rate should not be null in wallet #" + wallet2Number,
-                    tx1.getExchangeRate(), tx2.getExchangeRate());
-            }
+            if (tx1.getExchangeRate() == null) {
+                assertNull("Transaction " + tx1.getTxId() + " exchange rate should be null in wallet #" + wallet2Number,
+                    tx2.getExchangeRate());
+            } else {
+                assertNotNull("Transaction " + tx1.getTxId() + " exchange rate should not be null in wallet #" + wallet2Number,
+                    tx2.getExchangeRate());
+                assertEquals("Transaction " + tx1.getTxId() + " exchange rate coin should match",
+                    tx1.getExchangeRate().coin, tx2.getExchangeRate().coin);
+                assertEquals("Transaction " + tx1.getTxId() + " exchange rate fiat should match",
+                    tx1.getExchangeRate().fiat, tx2.getExchangeRate().fiat);
+            }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd73c93 and f3726fa.

📒 Files selected for processing (2)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1 hunks)
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
🔇 Additional comments (4)
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1)

448-464: LGTM! The shutdown logic correctly implements graceful termination with bounded waits.

The implementation properly:

  • Attempts graceful shutdown first with a 5-second timeout
  • Falls back to forced shutdown only after the timeout expires
  • Handles InterruptedException correctly by re-setting the interrupt flag
  • Always calls saveNow() after shutdown to ensure state persistence

This addresses the previous review concerns about inverted save logic and potential data loss.

core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (3)

47-59: LGTM!

The setup method properly loads the test wallet with appropriate exception handling. The switch to coinjoin-cache.wallet and verbose logging aligns with the test's benchmarking purpose.


335-348: LGTM!

Clean helper method for visual output in benchmarks.


418-507: LGTM!

Good use of @Ignore for this expensive benchmark test. The division by zero guard on line 482 is correctly implemented, and the test provides valuable consistency verification.

Copy link

@abaranouski abaranouski left a comment

Choose a reason for hiding this comment

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

approved and tested

Copy link

@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: 1

♻️ Duplicate comments (2)
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1)

448-454: Make threadPool shutdown graceful (bounded wait) instead of always interrupting tasks

The current sequence

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

has a few problems:

  • shutdownNow() will run in almost all realistic cases, so running qrinfo tasks are very likely to be interrupted mid‑update.
  • Those tasks are responsible for quorumRotationState.processDiff(...), processMasternodeList(...), save(), and opComplete.set(true). Interrupting them can:
    • Drop or partially apply the latest quorum/masternode state.
    • Leave opComplete futures incomplete, potentially hanging callers.
    • Cause saveNow() to persist state before in‑flight tasks have finished updating it.
  • The comment “let it die naturally” contradicts the actual behavior of immediately calling shutdownNow().

Consider switching to a bounded graceful shutdown with a short await before forcing interrupts, and only then calling saveNow(); for example:

-            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.removePreMessageReceivedEventListener(preMessageReceivedEventListener);
+            threadPool.shutdown();
+            try {
+                // Wait briefly for in‑flight qrinfo tasks to complete gracefully
+                if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
+                    log.warn("ThreadPool did not terminate in time, forcing shutdown");
+                    threadPool.shutdownNow();
+                    if (!threadPool.awaitTermination(2, TimeUnit.SECONDS)) {
+                        log.error("ThreadPool did not terminate after shutdownNow");
+                    }
+                }
+            } catch (InterruptedException e) {
+                log.warn("Interrupted while waiting for threadPool termination", e);
+                threadPool.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+            saveNow(); // Persist final state after tasks have had a chance to finish

This gives ongoing diff processing a fair chance to complete and set their futures, bounds the close-time blocking, and ensures saveNow() runs after the pool has (mostly) quiesced.

core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)

268-269: saveOnNextBlock is still racy across threads (same concern as earlier review).

saveOnNextBlock is written in setSaveOnNextBlock without any synchronization or volatile, but read inside notifyNewBestBlock while holding lock. Without a happens‑before edge, other threads (e.g. UI) toggling this flag are not guaranteed to be seen by the block-processing thread, so “disable saves on next block” can be missed or applied late.

The earlier review already suggested guarding this flag with lock; the code still matches the pre‑fix shape.

Recommend aligning with that suggestion:

-    // save now on blocks with transactions
-    private boolean saveOnNextBlock = true;
+    // save now on blocks with transactions
+    @GuardedBy("lock")
+    private boolean saveOnNextBlock = true;
@@
-    public void setSaveOnNextBlock(boolean saveOnNextBlock) {
-        this.saveOnNextBlock = saveOnNextBlock;
-    }
-
-    public boolean getSaveOnNextBlock() {
-        return saveOnNextBlock;
-    }
+    public void setSaveOnNextBlock(boolean saveOnNextBlock) {
+        lock.lock();
+        try {
+            this.saveOnNextBlock = saveOnNextBlock;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    public boolean getSaveOnNextBlock() {
+        lock.lock();
+        try {
+            return saveOnNextBlock;
+        } finally {
+            lock.unlock();
+        }
+    }

No changes are required to notifyNewBestBlock, as it already runs under lock.

Also applies to: 1860-1866, 2607-2613

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3726fa and 5c23bb9.

📒 Files selected for processing (2)
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1 hunks)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java (10 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
📚 Learning: 2025-09-18T22:12:09.613Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 286
File: core/src/main/java/org/bitcoinj/wallet/Wallet.java:6371-6373
Timestamp: 2025-09-18T22:12:09.613Z
Learning: In bitcoinj's Wallet.java, methods returning List<ECKey> can safely return new LinkedList<>(detkeys) where detkeys is List<DeterministicKey>, because DeterministicKey extends ECKey and Java's type inference handles this covariance correctly during list construction.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🔇 Additional comments (2)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

860-875: Locking in getKeyChainGroupSize() now respects wallet/keyChainGroup lock ordering.

Acquiring lock first and then keyChainGroupLock, and iterating keyChainExtensions only under lock, fixes the previous lock‑ordering and concurrent‑modification risks while still using the finer-grained keyChainGroupLock for the key-group calls. This aligns with the class comment (“lock > keyChainGroupLock”) and looks correct.


1938-1947: Simplified duplicate‑transaction check in isConsistentOrThrow() looks correct.

Computing expectedSize from the pool maps and then comparing to getTransactions(true).size() (which deduplicates by Transaction.equals/hashCode on txId) is a clean way to detect transactions present in multiple pools. Re‑entering lock inside getTransactions(true) is safe with ReentrantLock. No issues here.

Comment on lines +170 to +173
// Index mapping outpoints to transaction hashes that spend them.
// Used to make findDoubleSpendsAgainst() O(I) instead of O(W × I).
@GuardedBy("lock")
private final Map<TransactionOutPoint, Set<Sha256Hash>> spentOutpointsIndex = new HashMap<>();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep spentOutpointsIndex consistent when transactions are removed to avoid stale entries and growth.

The index itself and its use in findDoubleSpendsAgainst look solid (O(I) and properly @GuardedBy("lock")), but there are a couple of lifecycle gaps where entries are left behind when transactions are removed:

  • cleanup() removes risky pending txs from pending and transactions, but never calls removeFromSpentOutpointsIndex(tx), so their outpoints stay in spentOutpointsIndex indefinitely.
  • clearTransactions(int fromHeight) with fromHeight == 0 calls clearTransactions() but does not clear spentOutpointsIndex; only reset() does so explicitly. After a replay via clearTransactions(0), the index can still hold outpoints for transactions that were fully dropped.

Functionally, lookups stay correct because every findDoubleSpendsAgainst is filtered through the candidates map, but the index will grow monotonically across cleanups/replays, which is undesirable for long‑running wallets.

I suggest:

  1. Clear the index in the core clearTransactions() method so both reset/replay paths stay in sync:
 private void clearTransactions() {
     unspent.clear();
     spent.clear();
     pending.clear();
     dead.clear();
     transactions.clear();
     myUnspents.clear();
+    spentOutpointsIndex.clear();
 }

and then simplify reset():

-            clearTransactions();
-            spentOutpointsIndex.clear();
+            clearTransactions();
  1. Keep the index up to date when cleanup() permanently drops a pending tx:
-                        i.remove();
-                        transactions.remove(tx.getTxId());
+                        i.remove();
+                        transactions.remove(tx.getTxId());
+                        removeFromSpentOutpointsIndex(tx);

This keeps spentOutpointsIndex bounded and makes its invariants (“only wallet-resident transactions are indexed”) match the pool/transactions maps.

Also applies to: 2252-2272, 2274-2286, 2796-2807, 3432-3436, 3544-3548, 3578-3585, 3621-3643

🤖 Prompt for AI Agents
In core/src/main/java/org/bitcoinj/wallet/Wallet.java around lines 170-173,
ensure spentOutpointsIndex is kept consistent when transactions are removed:
modify clearTransactions() to clear spentOutpointsIndex so both replay
(clearTransactions(0)) and normal clear paths drop indexed outpoints; simplify
reset() to rely on clearTransactions() (removing its explicit clear of
spentOutpointsIndex); and in cleanup() call removeFromSpentOutpointsIndex(tx)
whenever a pending transaction is permanently removed from pending/transactions
so stale entries do not accumulate.

Copy link

@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: 0

🧹 Nitpick comments (3)
core/src/main/java/org/bitcoinj/manager/DashSystem.java (3)

255-271: Make close() more robust against exceptions in individual component shutdowns

The added logging makes shutdown sequencing clearer, but as written any exception in one close(...) call (e.g., sporkManager.close(peerGroup)) will prevent the remaining managers from being closed and their corresponding log lines from being emitted.

Consider wrapping each close in its own try/catch and logging failures while continuing with the rest, e.g.:

-            log.info("Closing network spork configuration manager");
-            sporkManager.close(peerGroup);
+            try {
+                log.info("Closing network spork configuration manager");
+                sporkManager.close(peerGroup);
+            } catch (Exception e) {
+                log.warn("Error while closing network spork configuration manager", e);
+            }

and applying the same pattern for masternodeSync, masternodeListManager, instantSendManager, signingManager, chainLockHandler, quorumManager, and coinJoinManager. This avoids partial shutdowns due to one misbehaving component.


272-275: Defensively guard LLMQ thread interruption and reuse existing sync-flag helper

The new if (masternodeSync.hasSyncFlag(...)) block assumes both masternodeSync and llmqBackgroundThread are non-null. That is currently guaranteed by the init/closeDash ordering, but it’s a brittle assumption.

For extra safety and consistency with start()/shutdown(), consider:

  • Using getSyncFlags().contains(SYNC_INSTANTSENDLOCKS) (or DashSystem.hasSyncFlag(...)) rather than calling masternodeSync directly.
  • Guarding the thread before interrupting, in line with stopLLMQThread():
-            if(masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_INSTANTSENDLOCKS)) {
-                log.info("Stopping LLMQ background thread");
-                llmqBackgroundThread.interrupt();
-            }
+            if (hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_INSTANTSENDLOCKS)
+                    && llmqBackgroundThread != null
+                    && llmqBackgroundThread.isAlive()) {
+                log.info("Stopping LLMQ background thread");
+                llmqBackgroundThread.interrupt();
+            }

This keeps the behavior aligned with other lifecycle code and is more resilient to future changes in initialization order.


276-289: Align close()/shutdown() behavior for scheduled tasks and clarify PeerGroup lifecycle

In close() you now:

  • Remove the newBestBlockListener.
  • Optionally cancel scheduledMasternodeSync.
  • Close blockChain / headerChain.
  • Clear peerGroup reference.

A couple of points to tighten up:

  1. Scheduled task cancellation symmetry

shutdown() cancels all three scheduled tasks and nulls them:

scheduledMasternodeSync.cancel(false);
scheduledMasternodeSync = null;
scheduledNetFulfilled.cancel(false);
scheduledNetFulfilled = null;
scheduledGovernance.cancel(false);
scheduledGovernance = null;

close() only cancels scheduledMasternodeSync, and leaves scheduledNetFulfilled and scheduledGovernance untouched. If closeDash() is ever used without a prior shutdown(), these two tasks can continue running (and invoking doMaintenance() on objects you just closed), which is at least a reliability risk.

Two possible cleanups:

  • Either have close() call shutdown() first (and make shutdown() idempotent), or
  • Mirror the full cancellation here, with logs, e.g.:
-            if (scheduledMasternodeSync != null) {
-                log.info("Cancelling scheduled masternode sync task");
-                scheduledMasternodeSync.cancel(true);
-            }
+            if (scheduledMasternodeSync != null) {
+                log.info("Cancelling scheduled masternode sync task");
+                scheduledMasternodeSync.cancel(true);
+                scheduledMasternodeSync = null;
+            }
+            if (scheduledNetFulfilled != null) {
+                log.info("Cancelling scheduled net fulfilled maintenance task");
+                scheduledNetFulfilled.cancel(true);
+                scheduledNetFulfilled = null;
+            }
+            if (scheduledGovernance != null) {
+                log.info("Cancelling scheduled governance maintenance task");
+                scheduledGovernance.cancel(true);
+                scheduledGovernance = null;
+            }
  1. PeerGroup lifecycle vs. reference clearing

log.info("Clearing peer group reference"); peerGroup = null; only drops the reference; it doesn’t stop the PeerGroup itself. That may be correct if the caller owns PeerGroup lifecycle (e.g., calling peerGroup.stopAsync()/awaitTerminated() elsewhere), but it would be good to double-check that this is indeed guaranteed in all shutdown paths and document that expectation here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c23bb9 and 8589b35.

📒 Files selected for processing (1)
  • core/src/main/java/org/bitcoinj/manager/DashSystem.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
core/src/main/java/org/bitcoinj/manager/DashSystem.java (1)
core/src/main/java/org/bitcoinj/core/MasternodeSync.java (1)
  • MasternodeSync (30-616)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle

@HashEngineering HashEngineering merged commit f3bebd2 into master Dec 10, 2025
4 of 6 checks passed
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.

3 participants