From 695f2b8dd35bee2f8220c6fce2d39094ce482351 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 17 Apr 2025 19:58:12 -0400 Subject: [PATCH] fix startup error with localhost, support fallback from provided nodes --- .../haveno/core/api/XmrConnectionService.java | 98 +++++++++++-------- .../java/haveno/core/api/XmrLocalNode.java | 11 ++- .../haveno/core/app/HavenoHeadlessApp.java | 2 +- .../java/haveno/core/app/HavenoSetup.java | 14 +-- .../java/haveno/core/xmr/nodes/XmrNodes.java | 13 ++- .../resources/i18n/displayStrings.properties | 3 +- .../haveno/desktop/main/MainViewModel.java | 40 ++++++-- 7 files changed, 115 insertions(+), 66 deletions(-) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index dc38547df6..53eba276a0 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -75,9 +75,10 @@ public final class XmrConnectionService { private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes - public enum XmrConnectionError { + public enum XmrConnectionFallbackType { LOCAL, - CUSTOM + CUSTOM, + PROVIDED } private final Object lock = new Object(); @@ -97,7 +98,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>(); + private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -129,6 +130,7 @@ public final class XmrConnectionService { private Set excludedConnections = new HashSet<>(); private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private boolean fallbackApplied; + private boolean usedSyncingLocalNodeBeforeStartup; @Inject public XmrConnectionService(P2PService p2PService, @@ -156,7 +158,13 @@ public final class XmrConnectionService { p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onTorNodeReady() { - ThreadUtils.submitToPool(() -> initialize()); + ThreadUtils.submitToPool(() -> { + try { + initialize(); + } catch (Exception e) { + log.warn("Error initializing connection service, error={}\n", e.getMessage(), e); + } + }); } @Override public void onHiddenServicePublished() {} @@ -270,7 +278,7 @@ public final class XmrConnectionService { accountService.checkAccountOpen(); // user needs to authorize fallback on startup after using locally synced node - if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { + if (fallbackRequiredBeforeConnectionSwitch()) { log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); return null; } @@ -283,6 +291,10 @@ public final class XmrConnectionService { return bestConnection; } + private boolean fallbackRequiredBeforeConnectionSwitch() { + return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored()); + } + private void addLocalNodeIfIgnored(Collection ignoredConnections) { if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); } @@ -458,15 +470,20 @@ public final class XmrConnectionService { public void fallbackToBestConnection() { if (isShutDownStarted) return; - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { + fallbackApplied = true; + if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) { log.warn("Falling back to public nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + initializeConnections(); } else { log.warn("Falling back to provided nodes"); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + initializeConnections(); + if (getConnection() == null) { + log.warn("No provided nodes available, falling back to public nodes"); + fallbackToBestConnection(); + } } - fallbackApplied = true; - initializeConnections(); } // ------------------------------- HELPERS -------------------------------- @@ -578,8 +595,8 @@ public final class XmrConnectionService { setConnection(connection.getUri()); // reset error connecting to local node - if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { - connectionServiceError.set(null); + if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) { + connectionServiceFallbackType.set(null); } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { MoneroRpcConnection bestConnection = getBestConnection(); @@ -602,8 +619,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.getAllXmrNodes()) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -615,8 +634,10 @@ public final class XmrConnectionService { // add default connections for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { if (node.hasClearNetAddress()) { - MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); - addConnection(connection); + if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) { + MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority()); + addConnection(connection); + } } if (node.hasOnionAddress()) { MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); @@ -632,6 +653,11 @@ public final class XmrConnectionService { } } + // set if last node was locally syncing + if (!isInitialized) { + usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain(); + } + // set connection proxies log.info("TOR proxy URI: " + getProxyUri()); for (MoneroRpcConnection connection : connectionManager.getConnections()) { @@ -666,29 +692,16 @@ public final class XmrConnectionService { onConnectionChanged(connectionManager.getConnection()); } - private boolean lastUsedLocalSyncingNode() { - return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); - } - - public void startLocalNode() { + public void startLocalNode() throws Exception { // cannot start local node as seed node if (HavenoUtils.isSeedNode()) { throw new RuntimeException("Cannot start local node on seed node"); } - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - throw new RuntimeException(e); - } - } else { - throw new RuntimeException("Local node is not offline and used as last connection"); - } + // start local node + log.info("Starting local node"); + xmrLocalNode.start(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { @@ -768,7 +781,7 @@ public final class XmrConnectionService { try { // poll daemon - if (daemon == null) switchToBestConnection(); + if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection(); try { if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); lastInfo = daemon.getInfo(); @@ -778,16 +791,19 @@ public final class XmrConnectionService { if (isShutDownStarted) return; // invoke fallback handling on startup error - boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); + boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup; if (lastInfo == null && canFallback) { - if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); - if (lastUsedLocalSyncingNode()) { + if (usedSyncingLocalNodeBeforeStartup) { log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.LOCAL); + connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL); + } else if (isProvidedConnections()) { + log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage()); + connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED); } else { log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); - connectionServiceError.set(XmrConnectionError.CUSTOM); + connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM); } } return; @@ -808,7 +824,7 @@ public final class XmrConnectionService { // connected to daemon isConnected = true; - connectionServiceError.set(null); + connectionServiceFallbackType.set(null); // determine if blockchain is syncing locally boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 @@ -885,10 +901,14 @@ public final class XmrConnectionService { } private boolean isFixedConnection() { - return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; + return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; } private boolean isCustomConnections() { return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } + + private boolean isProvidedConnections() { + return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED; + } } diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 7295202c64..0928340d25 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -109,17 +109,18 @@ public class XmrLocalNode { public boolean shouldBeIgnored() { if (config.ignoreLocalXmrNode) return true; - // determine if local node is configured + // ignore if fixed connection is not local + if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode); + + // check if local node is within configuration boolean hasConfiguredLocalNode = false; for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { - if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { + if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) { hasConfiguredLocalNode = true; break; } } - if (!hasConfiguredLocalNode) return true; - - return false; + return !hasConfiguredLocalNode; } public void addListener(XmrLocalNodeListener listener) { diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 84bdcc746a..0bdac1abc1 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); - havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 19503fafd8..192e3870b7 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; -import haveno.core.api.XmrConnectionService.XmrConnectionError; +import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -159,7 +159,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionErrorHandler; + private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -431,9 +431,9 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceError().addListener((observable, oldValue, newValue) -> { - if (displayMoneroConnectionErrorHandler == null) return; - displayMoneroConnectionErrorHandler.accept(newValue); + getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); }); log.info("Init P2P network"); @@ -735,8 +735,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public ObjectProperty getConnectionServiceError() { - return xmrConnectionService.getConnectionServiceError(); + public ObjectProperty getConnectionServiceFallbackType() { + return xmrConnectionService.getConnectionServiceFallbackType(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index a8fa1ade26..c38ae9b411 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -184,10 +184,6 @@ public class XmrNodes { this.operator = operator; } - public boolean hasOnionAddress() { - return onionAddress != null; - } - public String getHostNameOrAddress() { if (hostName != null) return hostName; @@ -195,10 +191,19 @@ public class XmrNodes { return address; } + public boolean hasOnionAddress() { + return onionAddress != null; + } + public boolean hasClearNetAddress() { return hostName != null || address != null; } + public String getClearNetUri() { + if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address"); + return "http://" + getHostNameOrAddress() + ":" + port; + } + @Override public String toString() { return "onionAddress='" + onionAddress + '\'' + diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4a978381ed..7c8ab0d6f6 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2090,7 +2090,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun walletPasswordWindow.headline=Enter password to unlock xmrConnectionError.headline=Monero connection error -xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.providedNodes=Error connecting to provided Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.customNodes=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. xmrConnectionError.localNode.start=Start local node xmrConnectionError.localNode.start.error=Error starting local node diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 16cef449d6..03c38e99ab 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -337,7 +337,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { + havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> { if (connectionError == null) { if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); } else { @@ -349,7 +349,6 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .actionButtonText(Res.get("xmrConnectionError.localNode.start")) .onAction(() -> { log.warn("User has chosen to start local node."); - havenoSetup.getConnectionServiceError().set(null); new Thread(() -> { try { HavenoUtils.xmrConnectionService.startLocalNode(); @@ -359,16 +358,20 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .headLine(Res.get("xmrConnectionError.localNode.start.error")) .warning(e.getMessage()) .closeButtonText(Res.get("shared.close")) - .onClose(() -> havenoSetup.getConnectionServiceError().set(null)) + .onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null)) .show(); + } finally { + havenoSetup.getConnectionServiceFallbackType().set(null); } }).start(); }) .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) .onSecondaryAction(() -> { log.warn("User has chosen to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.shutDown")) .onClose(HavenoApp.getShutDownHandler()); @@ -376,16 +379,35 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener case CUSTOM: moneroConnectionErrorPopup = new Popup() .headLine(Res.get("xmrConnectionError.headline")) - .warning(Res.get("xmrConnectionError.customNode")) + .warning(Res.get("xmrConnectionError.customNodes")) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { - havenoSetup.getConnectionServiceError().set(null); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); }) .closeButtonText(Res.get("shared.no")) .onClose(() -> { log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceError().set(null); + havenoSetup.getConnectionServiceFallbackType().set(null); + }); + break; + case PROVIDED: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.providedNodes")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + new Thread(() -> { + HavenoUtils.xmrConnectionService.fallbackToBestConnection(); + havenoSetup.getConnectionServiceFallbackType().set(null); + }).start(); + }) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackType().set(null); }); break; }