From c1f4edb66abdd8e2051a7b308080de1e017d6c84 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 15 Jul 2026 15:45:08 -0700 Subject: [PATCH 1/3] fix: rate-limit full thread dumps on peer socket timeout Since 22.0.4, checkForBlockStoreTimeout() dumped every thread's stack on every socket timeout. When timeouts cluster (e.g. repeated masternode connection failures during CoinJoin mixing), this floods the log via the single shared timeout timer thread and can freeze the app. Now the full all-threads dump runs at most once per 10 minutes across all connections. Every timeout still logs the PeerGroup/NioClientManager thread stacks, the stack of any thread stuck in peekByteArray, and runs the SPVBlockStore freeze detection. Also takes a single getAllStackTraces() snapshot instead of two. Co-Authored-By: Claude Fable 5 --- .../org/bitcoinj/core/PeerSocketHandler.java | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java b/core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java index 85c0fddfa..e66364793 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java +++ b/core/src/main/java/org/bitcoinj/core/PeerSocketHandler.java @@ -38,8 +38,11 @@ import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import static com.google.common.base.Preconditions.*; @@ -143,50 +146,61 @@ protected void timeoutOccurred() { close(); } + // Full all-threads stack dumps are expensive and can flood the log when timeouts cluster + // (e.g. repeated masternode connection failures during CoinJoin), so allow at most one + // per interval across all connections. Network thread dumps and freeze detection still + // run on every timeout. + private static final long FULL_THREAD_DUMP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10); + private static final AtomicLong lastFullThreadDumpTime = new AtomicLong(0); + + private static boolean shouldLogFullThreadDump() { + long now = System.currentTimeMillis(); + long last = lastFullThreadDumpTime.get(); + return now - last >= FULL_THREAD_DUMP_INTERVAL_MS && lastFullThreadDumpTime.compareAndSet(last, now); + } + /** * Checks all thread stacks to detect if any thread is stuck in native I/O operations * (peekByteArray or SPVBlockStore operations that freeze on Android). * @return true if a blockstore timeout is detected */ 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); + boolean fullDump = shouldLogFullThreadDump(); + boolean blockStoreTimeout = false; - // Check if this thread is stuck in native peekByteArray or SPVBlockStore operations - String elementStr = element.toString(); - if (elementStr.contains("peekByteArray")) { - foundBlockingCall = true; - } + for (Map.Entry entry : Thread.getAllStackTraces().entrySet()) { + Thread thread = entry.getKey(); + StackTraceElement[] stackTrace = entry.getValue(); + String threadName = thread.getName(); + boolean networkThread = threadName.contains("PeerGroup Thread") || threadName.contains("NioClientManager"); + + // Check if this thread is stuck in native peekByteArray or SPVBlockStore operations + boolean foundBlockingCall = false; + for (StackTraceElement element : stackTrace) { + if (element.toString().contains("peekByteArray")) { + foundBlockingCall = true; + break; } + } - if (foundBlockingCall) { - log.error("CRITICAL: Thread '{}' is stuck in native I/O operation (peekByteArray/SPVBlockStore)", threadName); - } - } else { + // always dump a stuck thread's stack; other threads only on the rate-limited full dump + if (networkThread || foundBlockingCall || fullDump) { log.warn("Stack trace for thread '{}' (State: {}):", threadName, thread.getState()); for (StackTraceElement element : stackTrace) { log.warn(" at {}", element); } } - }); - - // 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; + + if (foundBlockingCall) { + blockStoreTimeout = true; + if (networkThread) { + log.error("CRITICAL: Thread '{}' is stuck in native I/O operation (peekByteArray/SPVBlockStore)", threadName); + } else { + log.error("CRITICAL: Detected SPVBlockStore timeout - native I/O freeze detected in thread: {}", threadName); } } } - return false; + return blockStoreTimeout; } /** From 2c0649d23e24a4eec26287a0fe232c6c0795bcc0 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 26 Jul 2026 16:39:00 -0700 Subject: [PATCH 2/3] fix: guard against NPE when CoinJoin methods run during shutdown stopAsync() nulls masternodeGroup and close() nulls peerGroup while mixing coroutines and the maintenance timer may still be calling back into CoinJoinManager, crashing in startAsync(), addPendingMasternode(), forPeer() and disconnectMasternode(). Treat a nulled group as "already shut down" and no-op instead. Co-Authored-By: Claude Fable 5 --- .../bitcoinj/coinjoin/utils/CoinJoinManager.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java index d20a7a867..97ab6285a 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -323,16 +323,24 @@ public boolean isMasternodeOrDisconnectRequested(MasternodeAddress address) { } public boolean addPendingMasternode(CoinJoinClientSession session) { - return masternodeGroup.addPendingMasternode(session); + MasternodeGroup group = masternodeGroup; + return group != null && group.addPendingMasternode(session); } public boolean forPeer(MasternodeAddress address, MasternodeGroup.ForPeer forPeer, boolean warn) { - return masternodeGroup.forPeer(address, forPeer, warn); + MasternodeGroup group = masternodeGroup; + return group != null && group.forPeer(address, forPeer, warn); } public void startAsync() { lock.lock(); try { + // masternodeGroup is nulled by stopAsync() and peerGroup by close(); a mixing + // coroutine may still call this while the wallet service is shutting down. + if (masternodeGroup == null || peerGroup == null) { + log.info("coinjoin: startAsync called after shutdown; ignoring"); + return; + } if (!masternodeGroup.isRunning()) { log.info("coinjoin: broadcasting senddsq(true) to all peers"); peerGroup.shouldSendDsq(true); @@ -354,7 +362,8 @@ public void stopAsync() { } public boolean disconnectMasternode(Masternode service) { - return masternodeGroup.disconnectMasternode(service); + MasternodeGroup group = masternodeGroup; + return group != null && group.disconnectMasternode(service); } @VisibleForTesting From ac7b2a2ef041c7adb755d55ca264615c183921d2 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 11 Aug 2026 16:39:15 -0700 Subject: [PATCH 3/3] fix: handle missing transaction broadcaster when CoinJoin runs during shutdown PeerGroup.removeWallet() nulls the wallet's transaction broadcaster while a mixing coroutine may still be creating denominations or combining dust, so Wallet.sendCoins() throws IllegalStateException and kills the mixing thread. Catch it in TransactionBuilder.commit() and CoinJoinClientSession's dust combining so both fall into the existing soft-failure paths instead. Co-Authored-By: Claude Fable 5 --- .../java/org/bitcoinj/coinjoin/CoinJoinClientSession.java | 5 +++++ .../java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java index 0df3b0f39..afac104ea 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java @@ -240,6 +240,11 @@ private boolean combineOutputs(List vecTally, HashMap