From c236738ba1a46114cb4de47fd10acea267a1c3b0 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 18 Jul 2023 06:30:52 -0700 Subject: [PATCH 01/12] feat(coinjoin): add tracking of transactions --- .../java/org/bitcoinj/coinjoin/CoinJoin.java | 2 + .../coinjoin/CoinJoinClientManager.java | 40 ++++++++++- .../coinjoin/CoinJoinClientSession.java | 66 ++++++++++++++++--- .../org/bitcoinj/coinjoin/PoolMessage.java | 3 +- .../org/bitcoinj/coinjoin/PoolStatus.java | 10 ++- .../CoinJoinTransactionListener.java | 22 +++++++ .../listeners/CoinJoinTransactionType.java | 23 +++++++ .../listeners/SessionCompleteListener.java | 3 +- .../progress/MixingProgressTracker.java | 25 ++++++- .../coinjoin/utils/CoinJoinManager.java | 33 +++++++++- .../coinjoin/utils/CoinJoinReporter.java | 64 ++++++++++++++++-- .../coinjoin/utils/MasternodeGroup.java | 18 +++-- .../coinjoin/utils/TransactionBuilder.java | 6 ++ .../bitcoinj/wallet/CoinJoinExtension.java | 40 ++++++++++- .../coinjoin/TestWithMasternodeGroup.java | 2 +- .../java/org/bitcoinj/tools/WalletTool.java | 3 + 16 files changed, 326 insertions(+), 34 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionListener.java create mode 100644 core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionType.java diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java index cbb729ec95..34522bad66 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java @@ -333,6 +333,8 @@ public static String getMessageByID(PoolMessage nMessageID) { return "Inputs vs outputs size mismatch."; case ERR_TIMEOUT: return "Session has timed out."; + case ERR_CONNECTION_TIMEOUT: + return "Connection attempt has timed out (" + PendingDsaRequest.TIMEOUT + " ms)."; default: return "Unknown response."; } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java index 510045e355..239bf165c7 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java @@ -17,6 +17,7 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.SettableFuture; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener; import org.bitcoinj.coinjoin.listeners.MixingCompleteListener; import org.bitcoinj.coinjoin.listeners.MixingStartedListener; import org.bitcoinj.coinjoin.listeners.SessionCompleteListener; @@ -47,11 +48,9 @@ import java.util.Collections; import java.util.Deque; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; @@ -96,6 +95,8 @@ public class CoinJoinClientManager { = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList> mixingCompleteListeners = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList> transactionListeners + = new CopyOnWriteArrayList<>(); private boolean waitForAnotherBlock() { if (context.masternodeSync.hasSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_GOVERNANCE) && @@ -253,12 +254,16 @@ public boolean doAutomaticDenominating(boolean dryRun) { try { if (deqSessions.size() < CoinJoinClientOptions.getSessions()) { CoinJoinClientSession newSession = new CoinJoinClientSession(mixingWallet); + log.info("creating new session: {}: ", newSession.getId()); for (ListenerRegistration listener : sessionCompleteListeners) { newSession.addSessionCompleteListener(listener.executor, listener.listener); } for (ListenerRegistration listener : sessionStartedListeners) { newSession.addSessionStartedListener(listener.executor, listener.listener); } + for (ListenerRegistration listener : transactionListeners) { + newSession.addTransationListener (listener.executor, listener.listener); + } deqSessions.addLast(newSession); } for (CoinJoinClientSession session: deqSessions) { @@ -595,6 +600,37 @@ public void run() { } } + /** + * Adds an event listener object. Methods on this object are called when something interesting happens, + * like receiving money. Runs the listener methods in the user thread. + */ + public void addTransationListener (CoinJoinTransactionListener listener) { + addTransationListener (Threading.USER_THREAD, listener); + } + + /** + * Adds an event listener object. Methods on this object are called when something interesting happens, + * like receiving money. The listener is executed by the given executor. + */ + public void addTransationListener (Executor executor, CoinJoinTransactionListener listener) { + // This is thread safe, so we don't need to take the lock. + transactionListeners.add(new ListenerRegistration<>(listener, executor)); + for (CoinJoinClientSession session: deqSessions) { + session.addTransationListener (executor, listener); + } + } + + /** + * Removes the given event listener object. Returns true if the listener was removed, false if that listener + * was never added. + */ + public boolean removeTransationListener(CoinJoinTransactionListener listener) { + for (CoinJoinClientSession session: deqSessions) { + session.removeTransactionListener(listener); + } + return ListenerRegistration.removeFromList(listener, transactionListeners); + } + public List getSessionsStatus() { ArrayList sessionsStatus = Lists.newArrayList(); for (CoinJoinClientSession session : deqSessions) { diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java index fe2898732d..0d32a91266 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java @@ -16,6 +16,8 @@ package org.bitcoinj.coinjoin; import com.google.common.collect.Lists; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionType; import org.bitcoinj.coinjoin.listeners.SessionCompleteListener; import org.bitcoinj.coinjoin.listeners.SessionStartedListener; import org.bitcoinj.coinjoin.utils.CompactTallyItem; @@ -26,6 +28,7 @@ import org.bitcoinj.coinjoin.utils.TransactionBuilderOutput; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.MasternodeAddress; import org.bitcoinj.core.MasternodeSync; import org.bitcoinj.core.Message; import org.bitcoinj.core.NetworkParameters; @@ -72,6 +75,7 @@ import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_ENTRY_MAX_SIZE; import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_QUEUE_TIMEOUT; import static org.bitcoinj.coinjoin.CoinJoinConstants.COINJOIN_SIGNING_TIMEOUT; +import static org.bitcoinj.coinjoin.PoolMessage.ERR_CONNECTION_TIMEOUT; import static org.bitcoinj.coinjoin.PoolMessage.ERR_SESSION; import static org.bitcoinj.coinjoin.PoolMessage.ERR_TIMEOUT; import static org.bitcoinj.coinjoin.PoolMessage.MSG_POOL_MAX; @@ -109,6 +113,8 @@ public class CoinJoinClientSession extends CoinJoinBaseSession { = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList> sessionCompleteListeners = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList> transactionListeners + = new CopyOnWriteArrayList<>(); /// Create denominations private boolean createDenominated(Coin balanceToDenominate) { @@ -361,6 +367,8 @@ public int process(Coin amount) { log.info("coinjoin: txid: {}", strResult); + queueCreateDenominationListeners(txBuilder.getTransaction(), CoinJoinTransactionType.CREATE_DENOMINATION); + return true; } @@ -588,7 +596,7 @@ private boolean joinExistingQueue(Coin balanceNeedsAnonymized) { mixingWallet.getContext().coinJoinManager.addPendingMasternode(this); setState(POOL_STATE_QUEUE); timeLastSuccessfulStep.set(Utils.currentTimeSeconds()); - log.info("coinjoin: pending connection (from queue): sessionDenom: {} ({}), addr={}", + log.info("coinjoin: join existing queue -> pending connection, sessionDenom: {} ({}), addr={}", sessionDenom, CoinJoin.denominationToString(sessionDenom), dmn.getService()); setStatus(PoolStatus.CONNECTING); return true; @@ -643,7 +651,7 @@ private boolean startNewQueue(Coin balanceNeedsAnonymized) { continue; } - log.info("coinjoin: attempt {} connection to Masternode {}, {}", nTries, dmn.getService(), dmn.getProTxHash()); + log.info("coinjoin: attempt {} connection to masternode {}, protx: {}", nTries + 1, dmn.getService(), dmn.getProTxHash()); // try to get a single random denom out of setAmounts while (sessionDenom == 0) { @@ -817,7 +825,7 @@ public boolean process(Peer peer) { peer.sendMessage(entry); return true; } - })) { + }, true)) { log.info("coinjoin: failed to send to {} CoinJoinEntry: {}", mixingMasternode.getService().getSocketAddress(), entry); } } @@ -898,6 +906,7 @@ private void completedTransaction(PoolMessage messageID) { lock.lock(); try { setNull(); // this will also disconnect the masternode + setStatus(PoolStatus.COMPLETE); } finally { lock.unlock(); } @@ -1066,7 +1075,7 @@ protected void setNull() { } mixingMasternode = null; pendingDsaRequest = null; - log.info("session zeroed out"); + log.info("session zeroed out {}; {}", state, status); super.setNull(); } @@ -1074,6 +1083,10 @@ protected void setNull() { static int nextId = 0; private final int id; + public int getId() { + return id; + } + public CoinJoinClientSession(WalletEx mixingWallet) { super(mixingWallet.getContext()); this.mixingWallet = mixingWallet; @@ -1371,7 +1384,7 @@ public boolean doAutomaticDenominating(boolean fDryRun) { } } else { if (!isMyCollateralValid || !CoinJoin.isCollateralValid(txMyCollateral)) { - log.info("coinjoin: invalid collateral, recreating..."); + log.info("coinjoin: invalid collateral, recreating... [id: {}] ", id); if (!createCollateralTransaction(txMyCollateral, strReason)) { log.info("coinjoin: create collateral error: {}", strReason); return false; @@ -1482,12 +1495,14 @@ public boolean process(Peer peer) { log.info("sending {} to {}", pendingDsaRequest.getDsa(), peer); return true; } - }); + }, false); if (sentMessage) { pendingDsaRequest = null; } else if (pendingDsaRequest.isExpired()) { - log.info("coinjoin -- failed to connect to {}", pendingDsaRequest.getAddress()); + log.info("coinjoin -- failed to connect to {}; reason: expired", pendingDsaRequest.getAddress()); + setStatus(PoolStatus.CONNECTION_TIMEOUT); + queueSessionCompleteListeners(ERR_CONNECTION_TIMEOUT); setNull(); } @@ -1519,6 +1534,7 @@ public boolean checkTimeout() { setState(POOL_STATE_ERROR); queueSessionCompleteListeners(ERR_TIMEOUT); + setStatus(PoolStatus.TIMEOUT); unlockCoins(); keyHolderStorage.returnAll(); timeLastSuccessfulStep.set(Utils.currentTimeSeconds()); @@ -1627,7 +1643,41 @@ protected void queueSessionCompleteListeners(PoolMessage message) { registration.executor.execute(new Runnable() { @Override public void run() { - registration.listener.onSessionComplete(mixingWallet, getSessionID(), getSessionDenom(), message); + MasternodeAddress address = mixingMasternode.getService(); + registration.listener.onSessionComplete(mixingWallet, getSessionID(), getSessionDenom(), message, address); + } + }); + } + } + + public void addTransationListener(CoinJoinTransactionListener listener) { + addTransationListener (Threading.USER_THREAD, listener); + } + + /** + * Adds an event listener object. Methods on this object are called when something interesting happens, + * like receiving money. The listener is executed by the given executor. + */ + public void addTransationListener(Executor executor, CoinJoinTransactionListener listener) { + // This is thread safe, so we don't need to take the lock. + transactionListeners.add(new ListenerRegistration<>(listener, executor)); + } + + /** + * Removes the given event listener object. Returns true if the listener was removed, false if that listener + * was never added. + */ + public boolean removeTransactionListener(CoinJoinTransactionListener listener) { + return ListenerRegistration.removeFromList(listener, transactionListeners); + } + + protected void queueCreateDenominationListeners(Transaction denominationTransaction, CoinJoinTransactionType type) { + //checkState(lock.isHeldByCurrentThread()); + for (final ListenerRegistration registration : transactionListeners) { + registration.executor.execute(new Runnable() { + @Override + public void run() { + registration.listener.onTransactionProcessed(denominationTransaction, type); } }); } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/PoolMessage.java b/core/src/main/java/org/bitcoinj/coinjoin/PoolMessage.java index 931c647347..01cf4dd818 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/PoolMessage.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/PoolMessage.java @@ -41,7 +41,8 @@ public enum PoolMessage { MSG_POOL_MAX(ERR_SIZE_MISMATCH.value), // extra values for DASHJ Reporting - ERR_TIMEOUT(23); + ERR_TIMEOUT(23), + ERR_CONNECTION_TIMEOUT(24); public final int value; diff --git a/core/src/main/java/org/bitcoinj/coinjoin/PoolStatus.java b/core/src/main/java/org/bitcoinj/coinjoin/PoolStatus.java index 6d8f788c6b..ba47b8a2ae 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/PoolStatus.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/PoolStatus.java @@ -20,8 +20,12 @@ public enum PoolStatus { WARMUP(0x0001), IDLE(0x0002), CONNECTING(0x0003), - MIXING(0x0004), - FINISHED(0x1005), + CONNECTED(0x0004), + MIXING(0x0005), + COMPLETE(0x0106), + FINISHED(0x1007), + TIMEOUT(0x0107), + CONNECTION_TIMEOUT(0x0108), // Errors ERR_NO_INPUTS(0x2100), ERR_MASTERNODE_NOT_FOUND(0x2101), @@ -35,6 +39,7 @@ public enum PoolStatus { private static final int STOP = 0x1000; private static final int ERROR = 0x2000; private static final int WARNING = 0x4000; + private static final int COMPLETED = 0x0100; final int value; PoolStatus(int value) { @@ -44,4 +49,5 @@ public enum PoolStatus { public boolean isError() { return (value & ERROR) != 0; } public boolean isWarning() { return (value & WARNING) != 0; } public boolean shouldStop() { return (value & STOP) != 0; } + public boolean sessionCompleted() { return (value & COMPLETED) != 0; } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionListener.java b/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionListener.java new file mode 100644 index 0000000000..c7712b5d3d --- /dev/null +++ b/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Dash Core Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bitcoinj.coinjoin.listeners; + +import org.bitcoinj.core.Transaction; + +public interface CoinJoinTransactionListener { + void onTransactionProcessed(Transaction denominationTx, CoinJoinTransactionType type); +} diff --git a/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionType.java b/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionType.java new file mode 100644 index 0000000000..1624615ed8 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/coinjoin/listeners/CoinJoinTransactionType.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Dash Core Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.bitcoinj.coinjoin.listeners; + +public enum CoinJoinTransactionType { + CREATE_DENOMINATION, + CREATE_COLLATERAL, + MIXING_FEE, + COINJOIN, +} diff --git a/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java b/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java index 66764ce15b..a1ad2b2bce 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java @@ -16,8 +16,9 @@ package org.bitcoinj.coinjoin.listeners; import org.bitcoinj.coinjoin.PoolMessage; +import org.bitcoinj.core.MasternodeAddress; import org.bitcoinj.wallet.WalletEx; public interface SessionCompleteListener { - void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message); + void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address); } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java b/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java index da48c44aea..4b88042f9e 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java @@ -20,19 +20,28 @@ import org.bitcoinj.coinjoin.PoolMessage; import org.bitcoinj.coinjoin.PoolStatus; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionType; import org.bitcoinj.coinjoin.listeners.MixingCompleteListener; import org.bitcoinj.coinjoin.listeners.SessionCompleteListener; import org.bitcoinj.coinjoin.listeners.SessionStartedListener; +import org.bitcoinj.core.MasternodeAddress; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletEx; import java.util.List; import java.util.concurrent.ExecutionException; -public class MixingProgressTracker implements SessionStartedListener, SessionCompleteListener, MixingCompleteListener { +public class MixingProgressTracker implements SessionStartedListener, SessionCompleteListener, MixingCompleteListener, + NewBestBlockListener, CoinJoinTransactionListener { protected int completedSessions = 0; protected int timedOutSessions = 0; + protected int timedOutConnections = 0; private double lastPercent = 0; private final SettableFuture future = SettableFuture.create(); @@ -42,10 +51,12 @@ public MixingProgressTracker() { } @Override - public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message) { + public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address) { if (message == PoolMessage.MSG_SUCCESS) { completedSessions++; lastPercent = calculatePercentage(wallet); + } else if (message == PoolMessage.ERR_CONNECTION_TIMEOUT) { + timedOutConnections++; } else { timedOutSessions++; } @@ -89,4 +100,14 @@ public void await() throws InterruptedException { public void onSessionStarted(WalletEx wallet, int sessionId, int denomination, PoolMessage message) { } + + @Override + public void notifyNewBestBlock(StoredBlock block) throws VerificationException { + + } + + @Override + public void onTransactionProcessed(Transaction denominationTx, CoinJoinTransactionType type) { + + } } 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 b6d36bd3bd..422347bfd9 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -29,6 +29,7 @@ import org.bitcoinj.coinjoin.CoinJoinStatusUpdate; import org.bitcoinj.coinjoin.callbacks.RequestDecryptedKey; import org.bitcoinj.coinjoin.callbacks.RequestKeyParameter; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener; import org.bitcoinj.coinjoin.listeners.MixingCompleteListener; import org.bitcoinj.coinjoin.listeners.MixingStartedListener; import org.bitcoinj.coinjoin.listeners.SessionCompleteListener; @@ -186,8 +187,8 @@ public boolean addPendingMasternode(CoinJoinClientSession session) { return masternodeGroup.addPendingMasternode(session); } - public boolean forPeer(MasternodeAddress address, MasternodeGroup.ForPeer forPeer) { - return masternodeGroup.forPeer(address, forPeer); + public boolean forPeer(MasternodeAddress address, MasternodeGroup.ForPeer forPeer, boolean warn) { + return masternodeGroup.forPeer(address, forPeer, warn); } public void startAsync() { @@ -330,6 +331,34 @@ public void removeMixingCompleteListener(MixingCompleteListener listener) { } } + /** + * Adds an event listener object. Methods on this object are called when something interesting happens, + * like receiving money. Runs the listener methods in the user thread. + */ + public void addTransationListener (CoinJoinTransactionListener listener) { + addTransationListener (Threading.USER_THREAD, listener); + } + + /** + * Adds an event listener object. Methods on this object are called when something interesting happens, + * like receiving money. The listener is executed by the given executor. + */ + public void addTransationListener (Executor executor, CoinJoinTransactionListener listener) { + for (CoinJoinClientManager manager : coinJoinClientManagers.values()) { + manager.addTransationListener (executor, listener); + } + } + + /** + * Removes the given event listener object. Returns true if the listener was removed, false if that listener + * was never added. + */ + public void removeTransactionListener(CoinJoinTransactionListener listener) { + for (CoinJoinClientManager manager : coinJoinClientManagers.values()) { + manager.removeTransationListener(listener); + } + } + public void setRequestKeyParameter(RequestKeyParameter requestKeyParameter) { this.requestKeyParameter = requestKeyParameter; } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java index 4e941166c5..94b98d0b4b 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java @@ -20,8 +20,14 @@ import org.bitcoinj.coinjoin.CoinJoin; import org.bitcoinj.coinjoin.PoolMessage; import org.bitcoinj.coinjoin.PoolStatus; +import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionType; import org.bitcoinj.coinjoin.progress.MixingProgressTracker; +import org.bitcoinj.core.MasternodeAddress; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VerificationException; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletEx; @@ -34,7 +40,8 @@ import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.concurrent.TimeUnit; +import java.util.Map; +import java.util.TreeMap; public class CoinJoinReporter extends MixingProgressTracker { @@ -103,8 +110,8 @@ public void onSessionStarted(WalletEx wallet, int sessionId, int denomination, P } @Override - public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message) { - super.onSessionComplete(wallet, sessionId, denomination, message); + public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address) { + super.onSessionComplete(wallet, sessionId, denomination, message, address); try { writeTime(); Stopwatch watch = sessionMap.get(sessionId); @@ -112,13 +119,18 @@ public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, watch.stop(); if (message == PoolMessage.MSG_SUCCESS) { writer.write("Session Complete: "); - writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination), denomination)); + writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); writeWatch(watch); writer.newLine(); writeStats(wallet); + } else if (message == PoolMessage.ERR_CONNECTION_TIMEOUT) { + writer.write("Session Failure to connect: "); + writer.write(String.format("address: %s, denom: %s[%d]", address.getSocketAddress(), CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); + writeWatch(watch); + writer.write(CoinJoin.getMessageByID(message)); } else { writer.write("Session Failure: "); - writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination), denomination)); + writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); writeWatch(watch); writer.write(CoinJoin.getMessageByID(message)); } @@ -138,7 +150,8 @@ public void onMixingComplete(WalletEx wallet, List statusList) { writer.newLine(); writer.write(" status: " + Utils.listToString(statusList)); writer.newLine(); - writer.write(String.format(" timeouts: %d, completed: %d", timedOutSessions, completedSessions)); + writer.write(String.format(" connection timeouts: %d, session timeouts: %d, completed: %d", + timedOutConnections, timedOutSessions, completedSessions)); writer.newLine(); writeBalance(wallet); writer.newLine(); @@ -158,4 +171,43 @@ private void writeStats(WalletEx wallet) throws IOException { private void writeWatch(@Nullable Stopwatch watch) throws IOException { writer.write(String.format(" - %s ", watch != null ? watch : "N/A")); } + + @Override + public void notifyNewBestBlock(StoredBlock block) throws VerificationException { + super.notifyNewBestBlock(block); + try { + writeTime(); + writer.write("Block Mined: " + block.getHeight()); + writer.newLine(); + } catch (IOException x) { + throw new RuntimeException(x); + } + } + + @Override + public void onTransactionProcessed(Transaction denominationTx, CoinJoinTransactionType type) { + super.onTransactionProcessed(denominationTx, type); + try { + if (type == CoinJoinTransactionType.CREATE_DENOMINATION) { + writeTime(); + writer.write("Denominations Created: " + denominationTx.getTxId()); + writer.newLine(); + TreeMap denomMap = Maps.newTreeMap(); + for (TransactionOutput output : denominationTx.getOutputs()) { + int denom = CoinJoin.amountToDenomination(output.getValue()); + denomMap.merge(denom, 1, Integer::sum); + } + for (Map.Entry entry : denomMap.entrySet()) { + writer.write(" " + CoinJoin.denominationToString(entry.getKey()) + ": " + entry.getValue()); + writer.newLine(); + } + } else if (type == CoinJoinTransactionType.CREATE_COLLATERAL) { + writeTime(); + writer.write("Collateral Created: " + denominationTx.getTxId()); + writer.newLine(); + } + } catch (IOException x) { + throw new RuntimeException(x); + } + } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index 659df7909f..6ca2b1ad21 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -166,10 +166,7 @@ public boolean addPendingMasternode(CoinJoinClientSession session) { int maxConnections = getMaxConnections(); pendingMasternodesLock.lock(); try { - //if (pendingSessions.containsKey(mninfo.getSession())) - // return false; log.info("adding masternode for mixing. maxConnections = {}, protx: {}", maxConnections, session.getMixingMasternodeInfo().getProTxHash()); - log.info(" mixingMasternode match protxhash: {}", session.getMixingMasternodeInfo().getProTxHash().equals(session.getMixingMasternodeInfo().getProTxHash())); pendingSessions.add(session); masternodeMap.put(session.getMixingMasternodeInfo().getProTxHash(), session); } finally { @@ -251,7 +248,7 @@ public boolean process(Peer peer) { peer.close(); return true; } - }); + }, true); } @Override @@ -319,7 +316,7 @@ private boolean isNodeConnected(PeerAddress address) { public boolean process(Peer peer) { return peer.getAddress().equals(address); } - }); + }, false); } private boolean isNodePending(PeerAddress address) { @@ -378,7 +375,7 @@ public interface ForPeer { boolean process(Peer peer); } - public boolean forPeer(MasternodeAddress service, ForPeer predicate) { + public boolean forPeer(MasternodeAddress service, ForPeer predicate, boolean warn) { Preconditions.checkNotNull(service); List peerList = getConnectedPeers(); StringBuilder listOfPeers = new StringBuilder(); @@ -388,7 +385,14 @@ public boolean forPeer(MasternodeAddress service, ForPeer predicate) { return predicate.process(peer); } } - log.info("cannot find {} in the list of connected peers: {}", service.getSocketAddress(), listOfPeers); + if (warn) { + if (!isNodePending(new PeerAddress(params, service.getSocketAddress()))) { + log.info("cannot find {} in the list of connected peers: {}", service.getSocketAddress(), listOfPeers); + new Exception("cannot find " + service.getSocketAddress()).printStackTrace(); + } else { + log.info("{} in the list of pending peers: {}", service.getSocketAddress(), listOfPeers); + } + } return false; } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java index 6476cbb1b6..3a0c8d358b 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/TransactionBuilder.java @@ -63,6 +63,8 @@ public class TransactionBuilder { private final ArrayList vecOutputs = new ArrayList<>(); /// Needed by CTransactionBuilderOutput::UpdateAmount to lock cs_outputs + private Transaction transaction; + public TransactionBuilder(WalletEx wallet, final CompactTallyItem tallyItem) { this.wallet = wallet; dummyReserveDestination = new ReserveDestination(wallet); @@ -174,6 +176,7 @@ public boolean commit(StringBuilder strResult) { SendRequest request = SendRequest.forTx(req.tx); request.aesKey = wallet.getContext().coinJoinManager.requestKeyParameter(wallet); wallet.sendCoins(request); + transaction = request.tx; } catch (InsufficientMoneyException x) { throw new RuntimeException(x); } @@ -302,4 +305,7 @@ static int calculateMaximumSignedTxSize(Transaction tx, Wallet wallet, List> getOutputs() { + TreeMap> outputs = Maps.newTreeMap(); + for (Coin amount : CoinJoin.getStandardDenominations()) { + outputs.put(CoinJoin.amountToDenomination(amount), Lists.newArrayList()); + } + outputs.put(0, Lists.newArrayList()); + for (TransactionOutput output : wallet.getUnspents()) { + byte [] pkh = ScriptPattern.extractHashFromP2PKH(output.getScriptPubKey()); + if (getKeyChainGroup().findKeyFromPubKeyHash(pkh, Script.ScriptType.P2PKH) != null) { + int denom = CoinJoin.amountToDenomination(output.getValue()); + List listDenoms = outputs.get(denom); + listDenoms.add(output); + } + } + return outputs; + } + @Override public String toString(boolean includeLookahead, boolean includePrivateKeys, @Nullable KeyParameter aesKey) { - return "COINJOIN:\n Rounds: " + rounds + "\n" + - super.toString(includeLookahead, includePrivateKeys, aesKey); + StringBuilder builder = new StringBuilder(); + builder.append("COINJOIN:\n Rounds: ").append(rounds).append("\n"); + builder.append(super.toString(includeLookahead, includePrivateKeys, aesKey)).append("\n"); + builder.append("Outputs:\n"); + + for (Map.Entry> entry : getOutputs().entrySet()) { + int denom = entry.getKey(); + List outputs = entry.getValue(); + Coin value = outputs.stream().map(TransactionOutput::getValue).reduce(Coin::add).orElse(Coin.ZERO); + builder.append(CoinJoin.denominationToString(denom)).append(" ").append(outputs.size()).append(" ") + .append(value.toFriendlyString()).append("\n"); + } + return builder.toString(); } @Override diff --git a/core/src/test/java/org/bitcoinj/coinjoin/TestWithMasternodeGroup.java b/core/src/test/java/org/bitcoinj/coinjoin/TestWithMasternodeGroup.java index cc883efcdb..d41f321468 100644 --- a/core/src/test/java/org/bitcoinj/coinjoin/TestWithMasternodeGroup.java +++ b/core/src/test/java/org/bitcoinj/coinjoin/TestWithMasternodeGroup.java @@ -134,7 +134,7 @@ public boolean addPendingMasternode(CoinJoinClientSession session) { } @Override - public boolean forPeer(MasternodeAddress service, ForPeer predicate) { + public boolean forPeer(MasternodeAddress service, ForPeer predicate, boolean warn) { // this test will only connect to one "masternode" if (lastMasternode != null) { return predicate.process(lastMasternode.peer); diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 0cef572bf8..0950ff0225 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -1662,6 +1662,8 @@ private static void mix() { wallet.getContext().coinJoinManager.addSessionStartedListener(Threading.SAME_THREAD, reporter); wallet.getContext().coinJoinManager.addSessionCompleteListener(Threading.SAME_THREAD, reporter); wallet.getContext().coinJoinManager.addMixingCompleteListener(Threading.SAME_THREAD, reporter); + wallet.getContext().coinJoinManager.addTransationListener (Threading.SAME_THREAD, reporter); + wallet.getContext().blockChain.addNewBestBlockListener(Threading.SAME_THREAD, reporter); // mix coins try { @@ -1692,6 +1694,7 @@ private static void mix() { wallet.getContext().coinJoinManager.removeSessionCompleteListener(reporter); wallet.getContext().coinJoinManager.removeMixingCompleteListener(reporter); wallet.getContext().coinJoinManager.removeSessionStartedListener(reporter); + wallet.getContext().coinJoinManager.removeTransactionListener(reporter); wallet.getContext().coinJoinManager.stop(); } catch (ExecutionException | InterruptedException x) { throw new RuntimeException(x); From 26cf42a5d314abd958536dbf2e28ba0c3e617278 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 21 Jul 2023 09:04:02 -0700 Subject: [PATCH 02/12] fix(coinjoin): add more logging to isCollateralValid --- .../java/org/bitcoinj/coinjoin/CoinJoin.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java index 34522bad66..1fac3b264a 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoin.java @@ -144,10 +144,14 @@ public static boolean isCollateralValid(Transaction txCollateral) { } public static boolean isCollateralValid(Transaction txCollateral, boolean checkInputs) { - if (txCollateral.getOutputs().isEmpty()) + if (txCollateral.getOutputs().isEmpty()) { + log.info("coinjoin: Collateral invalid due to no outputs: {}", txCollateral.getTxId()); return false; - if (txCollateral.getLockTime() != 0) + } + if (txCollateral.getLockTime() != 0) { + log.info("coinjoin: Collateral invalid due to lock time != 0: {}", txCollateral.getTxId()); return false; + } Coin nValueIn = Coin.ZERO; Coin nValueOut = Coin.ZERO; @@ -180,10 +184,13 @@ public static boolean isCollateralValid(Transaction txCollateral, boolean checkI } } - log.info("coinjoin: collateral: {}", txCollateral); /* Continued */ - // the collateral tx must not have been seen on the network - return txCollateral.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.UNKNOWN; + boolean hasBeenSeen = txCollateral.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.UNKNOWN; + if (hasBeenSeen) { + log.info("coinjoin: collateral has been spent, need to recreate txCollateral={}", txCollateral.getTxId()); + } + log.info("coinjoin: collateral: {}", txCollateral); /* Continued */ + return hasBeenSeen; } public static Coin getCollateralAmount() { return getSmallestDenomination().div(10); } public static Coin getMaxCollateralAmount() { return getCollateralAmount().multiply(4); } From 7a2855d27b2184cca6b7c04c1b1753ab71352b1f Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 21 Jul 2023 09:06:41 -0700 Subject: [PATCH 03/12] feat(coinjoin): track state and joined status with onSessionComplete listener --- .../coinjoin/CoinJoinClientSession.java | 20 +++++++++++-------- .../listeners/SessionCompleteListener.java | 3 ++- .../progress/MixingProgressTracker.java | 4 +++- .../coinjoin/utils/CoinJoinReporter.java | 11 +++++----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java index 0d32a91266..2f6227c641 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientSession.java @@ -99,6 +99,7 @@ public class CoinJoinClientSession extends CoinJoinBaseSession { private String strAutoDenomResult; private Masternode mixingMasternode; + private boolean joined; // did we join a session (true), or start a session (false) private Transaction txMyCollateral; // client side collateral private boolean isMyCollateralValid = false; private PendingDsaRequest pendingDsaRequest; @@ -599,6 +600,7 @@ private boolean joinExistingQueue(Coin balanceNeedsAnonymized) { log.info("coinjoin: join existing queue -> pending connection, sessionDenom: {} ({}), addr={}", sessionDenom, CoinJoin.denominationToString(sessionDenom), dmn.getService()); setStatus(PoolStatus.CONNECTING); + joined = true; return true; } setStatus(PoolStatus.WARN_NO_MIXING_QUEUES); @@ -673,6 +675,7 @@ private boolean startNewQueue(Coin balanceNeedsAnonymized) { sessionDenom, CoinJoin.denominationToString(sessionDenom), dmn.getService()); context.coinJoinManager.startAsync(); setStatus(PoolStatus.CONNECTING); + joined = false; return true; } strAutoDenomResult = "Failed to start a new mixing queue"; @@ -858,7 +861,7 @@ private void processPoolStateUpdate(Peer peer, CoinJoinStatusUpdate statusUpdate keyHolderStorage.returnAll(); switch (statusUpdate.getMessageID()) { case ERR_INVALID_COLLATERAL: - log.error("coinjoin: collateral valid: {}", CoinJoin.isCollateralValid(txMyCollateral)); + log.error("coinjoin: collateral invalid: {}", CoinJoin.isCollateralValid(txMyCollateral)); isMyCollateralValid = false; setNull(); // for now lets disconnect. TODO: Why is the collateral invalid? break; @@ -895,7 +898,7 @@ private void setState(PoolState state) { private void completedTransaction(PoolMessage messageID) { if (messageID == MSG_SUCCESS) { log.info("coinjoin: CompletedTransaction -- success"); - queueSessionCompleteListeners(MSG_SUCCESS); + queueSessionCompleteListeners(getState(), MSG_SUCCESS); mixingWallet.getContext().coinJoinManager.coinJoinClientManagers.get(mixingWallet.getDescription()).updatedSuccessBlock(); keyHolderStorage.keepAll(); } else { @@ -1258,7 +1261,7 @@ public boolean doAutomaticDenominating(boolean fDryRun) { //status.set(PoolStatus.ERR_NO_MASTERNODES_DETECTED); //hasNothingToDo.set(true); setStatus(PoolStatus.ERR_NO_MASTERNODES_DETECTED); - queueSessionCompleteListeners(ERR_SESSION); + queueSessionCompleteListeners(getState(), ERR_SESSION); return false; } @@ -1292,7 +1295,7 @@ public boolean doAutomaticDenominating(boolean fDryRun) { // mixable balance is way too small if (nBalanceAnonymizable.isLessThan(nValueMin)) { setStatus(PoolStatus.ERR_NOT_ENOUGH_FUNDS); - queueSessionCompleteListeners(ERR_SESSION); + queueSessionCompleteListeners(getState(), ERR_SESSION); return false; } @@ -1502,7 +1505,7 @@ public boolean process(Peer peer) { } else if (pendingDsaRequest.isExpired()) { log.info("coinjoin -- failed to connect to {}; reason: expired", pendingDsaRequest.getAddress()); setStatus(PoolStatus.CONNECTION_TIMEOUT); - queueSessionCompleteListeners(ERR_CONNECTION_TIMEOUT); + queueSessionCompleteListeners(getState(), ERR_CONNECTION_TIMEOUT); setNull(); } @@ -1532,8 +1535,8 @@ public boolean checkTimeout() { log.info("coinjoin: {} {} timed out ({})", (state.get() == POOL_STATE_SIGNING) ? "Signing at session" : "Session", sessionID.get(), nTimeout); + queueSessionCompleteListeners(getState(), ERR_TIMEOUT); setState(POOL_STATE_ERROR); - queueSessionCompleteListeners(ERR_TIMEOUT); setStatus(PoolStatus.TIMEOUT); unlockCoins(); keyHolderStorage.returnAll(); @@ -1547,6 +1550,7 @@ public boolean checkTimeout() { public String toString() { return "CoinJoinClientSession{id=" + id + ", mixer=" + (mixingMasternode != null ? mixingMasternode.getService().getSocketAddress() : "none") + + ", joined=" + joined + ", msg='" + strLastMessage + '\'' + ", dsa=" + pendingDsaRequest + ", entries=" + entries.size() + @@ -1637,14 +1641,14 @@ public boolean removeSessionCompleteListener(SessionCompleteListener listener) { return ListenerRegistration.removeFromList(listener, sessionCompleteListeners); } - protected void queueSessionCompleteListeners(PoolMessage message) { + protected void queueSessionCompleteListeners(PoolState state, PoolMessage message) { //checkState(lock.isHeldByCurrentThread()); for (final ListenerRegistration registration : sessionCompleteListeners) { registration.executor.execute(new Runnable() { @Override public void run() { MasternodeAddress address = mixingMasternode.getService(); - registration.listener.onSessionComplete(mixingWallet, getSessionID(), getSessionDenom(), message, address); + registration.listener.onSessionComplete(mixingWallet, getSessionID(), getSessionDenom(), state, message, address, joined); } }); } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java b/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java index a1ad2b2bce..50da0dbb41 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/listeners/SessionCompleteListener.java @@ -16,9 +16,10 @@ package org.bitcoinj.coinjoin.listeners; import org.bitcoinj.coinjoin.PoolMessage; +import org.bitcoinj.coinjoin.PoolState; import org.bitcoinj.core.MasternodeAddress; import org.bitcoinj.wallet.WalletEx; public interface SessionCompleteListener { - void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address); + void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolState state, PoolMessage message, MasternodeAddress address, boolean joined); } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java b/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java index 4b88042f9e..0d4e564fe3 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/progress/MixingProgressTracker.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.SettableFuture; import org.bitcoinj.coinjoin.PoolMessage; +import org.bitcoinj.coinjoin.PoolState; import org.bitcoinj.coinjoin.PoolStatus; import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionListener; import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionType; @@ -51,7 +52,8 @@ public MixingProgressTracker() { } @Override - public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address) { + public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolState state, + PoolMessage message, MasternodeAddress address, boolean joined) { if (message == PoolMessage.MSG_SUCCESS) { completedSessions++; lastPercent = calculatePercentage(wallet); diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java index 94b98d0b4b..c245637f5e 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java @@ -19,6 +19,7 @@ import com.google.common.collect.Maps; import org.bitcoinj.coinjoin.CoinJoin; import org.bitcoinj.coinjoin.PoolMessage; +import org.bitcoinj.coinjoin.PoolState; import org.bitcoinj.coinjoin.PoolStatus; import org.bitcoinj.coinjoin.listeners.CoinJoinTransactionType; import org.bitcoinj.coinjoin.progress.MixingProgressTracker; @@ -110,8 +111,8 @@ public void onSessionStarted(WalletEx wallet, int sessionId, int denomination, P } @Override - public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolMessage message, MasternodeAddress address) { - super.onSessionComplete(wallet, sessionId, denomination, message, address); + public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, PoolState state, PoolMessage message, MasternodeAddress address, boolean joined) { + super.onSessionComplete(wallet, sessionId, denomination, state, message, address, joined); try { writeTime(); Stopwatch watch = sessionMap.get(sessionId); @@ -119,18 +120,18 @@ public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, watch.stop(); if (message == PoolMessage.MSG_SUCCESS) { writer.write("Session Complete: "); - writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); + writer.write(String.format("id: %d, denom: %s[%d], joined: %b", sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination, joined)); writeWatch(watch); writer.newLine(); writeStats(wallet); } else if (message == PoolMessage.ERR_CONNECTION_TIMEOUT) { writer.write("Session Failure to connect: "); - writer.write(String.format("address: %s, denom: %s[%d]", address.getSocketAddress(), CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); + writer.write(String.format("%s, address: %s, denom: %s[%d], joined: %b", state, address.getSocketAddress(), CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination, joined)); writeWatch(watch); writer.write(CoinJoin.getMessageByID(message)); } else { writer.write("Session Failure: "); - writer.write(String.format("id: %d, denom: %s[%d]", sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination)); + writer.write(String.format("%s, id: %d, denom: %s[%d], joined: %b", state, sessionId, CoinJoin.denominationToAmount(denomination).toFriendlyString(), denomination, joined)); writeWatch(watch); writer.write(CoinJoin.getMessageByID(message)); } From aa138f8dfa8dec783cbc9f813e424aa0118009e3 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 23 Jul 2023 23:15:34 -0700 Subject: [PATCH 04/12] fix(coinjoin): improve coinjoin stability with new connection manager thread --- .../coinjoin/CoinJoinClientManager.java | 4 + .../coinjoin/utils/CoinJoinManager.java | 5 + .../coinjoin/utils/MasternodeGroup.java | 173 +++++++++++++++++- .../java/org/bitcoinj/core/PeerGroup.java | 13 +- 4 files changed, 183 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java index 239bf165c7..df4ea3b801 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientManager.java @@ -107,6 +107,10 @@ private boolean waitForAnotherBlock() { return cachedBlockHeight - cachedLastSuccessBlock < minBlocksToWait; } + public boolean isWaitingForNewBlock() { + return waitForAnotherBlock(); + } + // Make sure we have enough keys since last backup private boolean checkAutomaticBackup() { return CoinJoinClientOptions.isEnabled() && isMixing(); 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 422347bfd9..0dae828200 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -159,6 +159,7 @@ public void stop() { public void initMasternodeGroup(AbstractBlockChain blockChain) { this.blockChain = blockChain; masternodeGroup = new MasternodeGroup(context, blockChain); + masternodeGroup.setCoinJoinManager(this); } NewBestBlockListener newBestBlockListener = new NewBestBlockListener() { @@ -374,4 +375,8 @@ public void setRequestDecryptedKey(RequestDecryptedKey requestDecryptedKey) { public @Nullable ECKey requestDecryptKey(ECKey key) { return requestDecryptedKey != null ? requestDecryptedKey.requestDecryptedKey(key) : null; } + + public boolean isWaitingForNewBlock() { + return coinJoinClientManagers.values().stream().anyMatch(CoinJoinClientManager::isWaitingForNewBlock); + } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index 6ca2b1ad21..361f7dd4c4 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -18,6 +18,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; import net.jcip.annotations.GuardedBy; import org.bitcoinj.coinjoin.CoinJoinClientOptions; import org.bitcoinj.coinjoin.CoinJoinClientSession; @@ -29,6 +30,10 @@ import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.evolution.Masternode; import org.bitcoinj.net.ClientConnectionManager; import org.bitcoinj.net.discovery.PeerDiscovery; @@ -45,17 +50,15 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; -import static java.lang.Math.max; import static java.lang.Math.min; -public class MasternodeGroup extends PeerGroup { +public class MasternodeGroup extends PeerGroup implements NewBestBlockListener { private static final Logger log = LoggerFactory.getLogger(MasternodeGroup.class); private final ReentrantLock pendingMasternodesLock = Threading.lock("pendingMasternodes"); @@ -90,6 +93,8 @@ public void shutdown() { } }; + private CoinJoinManager coinJoinManager; + /** * See {@link #MasternodeGroup(Context)} * @@ -118,6 +123,7 @@ public MasternodeGroup(Context context) { */ public MasternodeGroup(NetworkParameters params, @Nullable AbstractBlockChain chain) { super(params, chain); + init(); } /** @@ -129,6 +135,7 @@ public MasternodeGroup(NetworkParameters params, @Nullable AbstractBlockChain ch */ public MasternodeGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, @Nullable AbstractBlockChain headerChain) { super(params, chain, headerChain); + init(); } /** @@ -178,7 +185,11 @@ public boolean addPendingMasternode(CoinJoinClientSession session) { } - private final ExponentialBackoff.Params masternodeBackoffParams = new ExponentialBackoff.Params(1000, 1.001f, 10 * 1000); + + // Exponential backoff for peers starts at 1 second and maxes at 5 seconds. + private final ExponentialBackoff.Params masternodeBackoffParams = new ExponentialBackoff.Params(1000, 1.001f, 5 * 1000); + // Tracks failures globally in case of a network failure. + @GuardedBy("lock") private final ExponentialBackoff masternodeGroupBackoff = new ExponentialBackoff(new ExponentialBackoff.Params(1000, 1.5f, 2 * 1001)); // Adds peerAddress to backoffMap map and inactives queue. // Returns true if it was added, false if it was already there. @@ -192,8 +203,8 @@ protected boolean addInactive(PeerAddress peerAddress, int priority) { // do not connect to another the same peer twice if (isNodeConnected(peerAddress) || isNodeConnected(peerAddress)) { - log.info("attempting to connect to the same peer again: {}", peerAddress); - return false; // do not connect to the same p + log.info("attempting to connect to the same masternode again: {}", peerAddress); + return false; // do not connect to the same masternode } backoffMap.put(peerAddress, new ExponentialBackoff(masternodeBackoffParams)); @@ -224,12 +235,21 @@ private void updateMaxConnections() { } public boolean isMasternodeOrDisconnectRequested(MasternodeAddress addr) { - return forPeer(addr, new ForPeer() { + boolean found = forPeer(addr, new ForPeer() { @Override public boolean process(Peer peer) { return true; } - }); + }, false); + + if (!found) { + for (Masternode mn : pendingClosingMasternodes) { + if (mn.getService().equals(addr)) { + found = true; + } + } + } + return found; } public boolean disconnectMasternode(Masternode mn) { @@ -264,7 +284,13 @@ protected void handlePeerDeath(final Peer peer, @Nullable Throwable exception) { masternode != null ? masternode.getService().getSocketAddress() : "not found in closing list"); if (masternode != null) { pendingMasternodesLock.lock(); + PeerAddress address = peer.getAddress(); try { + if (pendingClosingMasternodes.contains(masternode)) { + // if this is part of pendingClosingMasternodes, where we want to close the connection, + // we don't want to increase the backoff time + backoffMap.get(address).trackSuccess(); + } pendingClosingMasternodes.remove(masternode); pendingSessions.remove(masternodeMap.get(masternode.getProTxHash())); masternodeMap.remove(masternode.getProTxHash()); @@ -305,7 +331,7 @@ protected Peer connectTo(PeerAddress address, boolean incrementMaxConnections, i } Peer peer = super.connectTo(address, incrementMaxConnections, connectTimeoutMillis); - log.info("masternode[connected] {}: {}; {}", peer.getAddress().getSocketAddress(), session.getMixingMasternodeInfo().getProTxHash(), session); + log.info("masternode[connecting] {}: {}; {}", peer.getAddress().getSocketAddress(), session.getMixingMasternodeInfo().getProTxHash(), session); return peer; } @@ -371,6 +397,10 @@ public CoinJoinClientSession getMixingSession(Peer masternode) { return addressMap.get(new MasternodeAddress(masternode.getAddress().getSocketAddress())); } + public void setCoinJoinManager(CoinJoinManager coinJoinManager) { + this.coinJoinManager = coinJoinManager; + } + public interface ForPeer { boolean process(Peer peer); } @@ -418,4 +448,129 @@ protected boolean isMasternodeSession(PeerAddress address) { pendingMasternodesLock.unlock(); } } + + @Override + protected void triggerConnections() { + // Run on a background thread due to the need to potentially retry and back off in the background. + if (!executor.isShutdown()) + executor.execute(triggerMasternodeConnectionsJob); + } + + private final Runnable triggerMasternodeConnectionsJob = new Runnable() { + private boolean firstRun = true; + private final static long MIN_PEER_DISCOVERY_INTERVAL = 1000L; + + @Override + public void run() { + try { + go(); + } catch (Throwable e) { + log.error("Exception when trying to build connections", e); // The executor swallows exceptions :( + } + } + + public void go() { + if (!isRunning()) return; + + if (coinJoinManager.isWaitingForNewBlock()) + return; + + boolean doDiscovery = false; + long now = Utils.currentTimeMillis(); + lock.lock(); + try { +// // First run: try and use a local node if there is one, for the additional security it can provide. +// // But, not on Android as there are none for this platform: it could only be a malicious app trying +// // to hijack our traffic. +// if (!Utils.isAndroidRuntime() && useLocalhostPeerWhenPossible && maybeCheckForLocalhostPeer() && firstRun) { +// log.info("Localhost peer detected, trying to use it instead of P2P discovery"); +// maxConnections = 0; +// connectToLocalHost(); +// return; +// } + + boolean havePeerWeCanTry = !inactives.isEmpty() && backoffMap.get(inactives.peek()).getRetryTime() <= now; + doDiscovery = !havePeerWeCanTry; + } finally { + firstRun = false; + lock.unlock(); + } + + // Don't hold the lock across discovery as this process can be very slow. + boolean discoverySuccess = false; + if (doDiscovery) { + discoverySuccess = discoverPeers() > 0; + } + + long retryTime; + PeerAddress addrToTry; + lock.lock(); + try { + if (doDiscovery) { + // Require that we have enough connections, to consider this + // a success, or we just constantly test for new peers + if (discoverySuccess && countConnectedAndPendingPeers() >= getMaxConnections()) { + masternodeGroupBackoff.trackSuccess(); + } else { + masternodeGroupBackoff.trackFailure(); + } + } + // Inactives is sorted by backoffMap time. + if (inactives.isEmpty()) { + if (countConnectedAndPendingPeers() < getMaxConnections()) { + long interval = Math.max(masternodeGroupBackoff.getRetryTime() - now, MIN_PEER_DISCOVERY_INTERVAL); + log.info("Masternode discovery didn't provide us any more masternodes, will try again in " + + interval + "ms."); + executor.schedule(this, interval, TimeUnit.MILLISECONDS); + } else { + // We have enough peers and discovery provided no more, so just settle down. Most likely we + // were given a fixed set of addresses in some test scenario. + } + return; + } else { + do { + addrToTry = inactives.poll(); + } while (isIpv6Unreachable() && addrToTry.getAddr() instanceof Inet6Address); + retryTime = backoffMap.get(addrToTry).getRetryTime(); + } + retryTime = Math.max(retryTime, masternodeGroupBackoff.getRetryTime()); + if (retryTime > now) { + long delay = retryTime - now; + log.info("Waiting {} ms before next connect attempt to masternode {}", delay, addrToTry == null ? "" : "to " + addrToTry); + inactives.add(addrToTry); + executor.schedule(this, delay, TimeUnit.MILLISECONDS); + return; + } + connectTo(addrToTry, false, getConnectTimeoutMillis()); + } finally { + lock.unlock(); + } + if (countConnectedAndPendingPeers() < getMaxConnections()) { + executor.execute(this); // Try next peer immediately. + } + } + }; + + @Override + public void notifyNewBestBlock(StoredBlock block) throws VerificationException { + log.info("New block found, restarting masternode connections job"); + triggerConnections(); + } + + @Override + public ListenableFuture startAsync() { + if (chain != null) { + chain.addNewBestBlockListener(this); + } + + return super.startAsync(); + } + + @Override + public ListenableFuture stopAsync() { + if (chain != null) { + chain.removeNewBestBlockListener(this); + } + return super.stopAsync(); + } } diff --git a/core/src/main/java/org/bitcoinj/core/PeerGroup.java b/core/src/main/java/org/bitcoinj/core/PeerGroup.java index 94939947ab..fe6a50e891 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerGroup.java +++ b/core/src/main/java/org/bitcoinj/core/PeerGroup.java @@ -178,6 +178,10 @@ public class PeerGroup implements TransactionBroadcaster, GovernanceVoteBroadcas @GuardedBy("lock") private boolean useLocalhostPeerWhenPossible = true; @GuardedBy("lock") private boolean ipv6Unreachable = false; + protected boolean isIpv6Unreachable() { + return ipv6Unreachable; + } + @GuardedBy("lock") private long fastCatchupTimeSecs; private final CopyOnWriteArrayList wallets; private final CopyOnWriteArrayList peerFilterProviders; @@ -340,7 +344,10 @@ public void onPeerDisconnected(Peer peer, int peerCount) { /** The default timeout between when a connection attempt begins and version message exchange completes */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000; private volatile int vConnectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; - + protected int getConnectTimeoutMillis() { + return vConnectTimeoutMillis; + } + /** Whether bloom filter support is enabled when using a non FullPrunedBlockchain*/ private volatile boolean vBloomFilteringEnabled = true; @@ -611,7 +618,7 @@ public void go() { } }; - private void triggerConnections() { + protected void triggerConnections() { // Run on a background thread due to the need to potentially retry and back off in the background. if (!executor.isShutdown()) executor.execute(triggerConnectionsJob); @@ -1169,7 +1176,7 @@ void waitForJobQueue() { Futures.getUnchecked(executor.submit(Runnables.doNothing())); } - private int countConnectedAndPendingPeers() { + protected int countConnectedAndPendingPeers() { lock.lock(); try { return peers.size() + pendingPeers.size(); From 1428ae51ad3eca5add9fbaac2db80e24ed31cd49 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 24 Jul 2023 15:34:44 -0700 Subject: [PATCH 05/12] fix(coinjoin): Make PendingDsaRequest.TIMEOUT public --- core/src/main/java/org/bitcoinj/coinjoin/PendingDsaRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/PendingDsaRequest.java b/core/src/main/java/org/bitcoinj/coinjoin/PendingDsaRequest.java index 5033d1313f..26b8827bb7 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/PendingDsaRequest.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/PendingDsaRequest.java @@ -21,7 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull; public class PendingDsaRequest { - private static final int TIMEOUT = 15; + public static final int TIMEOUT = 15; private MasternodeAddress addr; private CoinJoinAccept dsa; private long nTimeCreated = 0; From 6c5d99e0f6277a97397e06bef4b97c8b28be2a05 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 24 Jul 2023 15:51:43 -0700 Subject: [PATCH 06/12] chore: add run configuration for Regular Empty --- .run/WalletTool Regular Empty.run.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .run/WalletTool Regular Empty.run.xml diff --git a/.run/WalletTool Regular Empty.run.xml b/.run/WalletTool Regular Empty.run.xml new file mode 100644 index 0000000000..24ec679157 --- /dev/null +++ b/.run/WalletTool Regular Empty.run.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file From fe9cb38133dc8cd73183f8194cfb63301d55959d Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 30 Jul 2023 07:57:30 -0700 Subject: [PATCH 07/12] fix(core): stop copy blockchain at genesis block --- .../main/java/org/bitcoinj/core/PeerGroup.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/PeerGroup.java b/core/src/main/java/org/bitcoinj/core/PeerGroup.java index fe6a50e891..2ed743173e 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerGroup.java +++ b/core/src/main/java/org/bitcoinj/core/PeerGroup.java @@ -449,7 +449,7 @@ public int compare(PeerAddress a, PeerAddress b) { try { this.headerChain = new BlockChain(params, new MemoryBlockStore(params)); StoredBlock cursor = chain.getChainHead(); - while (cursor != null) { + while (cursor != null && !cursor.getHeader().equals(params.getGenesisBlock())) { this.headerChain.getBlockStore().put(cursor); cursor = cursor.getPrev(chain.getBlockStore()); } @@ -2562,12 +2562,16 @@ public void onSuccess(Transaction transaction) { if(transaction.getConfidence().numBroadcastPeers() == 0) { // TODO: this tx was sent to a single peer, should we send it again to make sure or see if there are more connections? - int sentCount = pendingTxSendCounts.get(transaction.getTxId()); + Integer sentCount = pendingTxSendCounts.get(transaction.getTxId()); - if(sentCount <= 2) { - log.info("resending tx {} since it was only sent to 1 peer", tx.getHash()); - broadcastTransaction(tx); - } else pendingTxSendCounts.put(tx.getHash(), sentCount + MAX_ATTEMPTS); + if (sentCount != null) { + if (sentCount <= 2) { + log.info("resending tx {} since it was only sent to 1 peer", tx.getHash()); + broadcastTransaction(tx); + } else { + pendingTxSendCounts.put(tx.getHash(), sentCount + MAX_ATTEMPTS); + } + } } pendingTxSendCounts.remove(tx.getHash()); } From ed932935f7ea696872fcabe5bb6b096d01980538 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 30 Jul 2023 08:12:51 -0700 Subject: [PATCH 08/12] fix(coinjoin): fix exception when watch was already stopped in CoinJoinReporter --- .../main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java index c245637f5e..27f3c4c8b2 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinReporter.java @@ -116,7 +116,7 @@ public void onSessionComplete(WalletEx wallet, int sessionId, int denomination, try { writeTime(); Stopwatch watch = sessionMap.get(sessionId); - if (watch != null) + if (watch != null && watch.isRunning()) watch.stop(); if (message == PoolMessage.MSG_SUCCESS) { writer.write("Session Complete: "); From 263177016501c37e4343c05cb46c613aa811a451 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 30 Jul 2023 16:21:41 -0700 Subject: [PATCH 09/12] fix(coinjoin): shutdown the connection executor when mixing is complete --- .../main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index 361f7dd4c4..e354f3c399 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -571,6 +571,9 @@ public ListenableFuture stopAsync() { if (chain != null) { chain.removeNewBestBlockListener(this); } + if (!executor.isShutdown()) { + executor.shutdown(); + } return super.stopAsync(); } } From 32a7fbcb5923be7500bf76cda70be4026ca23840 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 30 Jul 2023 18:19:34 -0700 Subject: [PATCH 10/12] Revert "fix(coinjoin): shutdown the connection executor when mixing is complete" This reverts commit 263177016501c37e4343c05cb46c613aa811a451. --- .../main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index e354f3c399..361f7dd4c4 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -571,9 +571,6 @@ public ListenableFuture stopAsync() { if (chain != null) { chain.removeNewBestBlockListener(this); } - if (!executor.isShutdown()) { - executor.shutdown(); - } return super.stopAsync(); } } From 919119f4150ee8584d13ec3e42d74cb5b7922b32 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 30 Jul 2023 18:39:43 -0700 Subject: [PATCH 11/12] fix(coinjoin): shutdown the connection executor when mixing is complete --- .../java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java | 4 ++++ .../java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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 0dae828200..2eb1763abc 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -379,4 +379,8 @@ public void setRequestDecryptedKey(RequestDecryptedKey requestDecryptedKey) { public boolean isWaitingForNewBlock() { return coinJoinClientManagers.values().stream().anyMatch(CoinJoinClientManager::isWaitingForNewBlock); } + + public boolean isMixing() { + return coinJoinClientManagers.values().stream().anyMatch(CoinJoinClientManager::isMixing); + } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index 361f7dd4c4..2ae0a9ae84 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -472,7 +472,7 @@ public void run() { public void go() { if (!isRunning()) return; - if (coinJoinManager.isWaitingForNewBlock()) + if (coinJoinManager.isWaitingForNewBlock() || !coinJoinManager.isMixing()) return; boolean doDiscovery = false; From 4294b3219e6d03f8f708c2009d6678f19a075be1 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 31 Jul 2023 21:58:52 -0700 Subject: [PATCH 12/12] chore(coinjoin): remove some commented code --- .../org/bitcoinj/coinjoin/utils/MasternodeGroup.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java index 2ae0a9ae84..8b8b49326e 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/MasternodeGroup.java @@ -479,16 +479,6 @@ public void go() { long now = Utils.currentTimeMillis(); lock.lock(); try { -// // First run: try and use a local node if there is one, for the additional security it can provide. -// // But, not on Android as there are none for this platform: it could only be a malicious app trying -// // to hijack our traffic. -// if (!Utils.isAndroidRuntime() && useLocalhostPeerWhenPossible && maybeCheckForLocalhostPeer() && firstRun) { -// log.info("Localhost peer detected, trying to use it instead of P2P discovery"); -// maxConnections = 0; -// connectToLocalHost(); -// return; -// } - boolean havePeerWeCanTry = !inactives.isEmpty() && backoffMap.get(inactives.peek()).getRetryTime() <= now; doDiscovery = !havePeerWeCanTry; } finally {