P2P status indicator with update prompt

Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
Co-authored-by: Christoph Atteneder <christoph.atteneder@gmail.com>
This commit is contained in:
napoly 2022-11-20 21:01:42 +01:00 committed by woodser
parent a2929035bc
commit bd70b935e4
13 changed files with 354 additions and 64 deletions

View file

@ -22,6 +22,11 @@ import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.mailbox.MailboxMessageService;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.MessageListener;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.peers.keepalive.messages.Ping;
import bisq.network.p2p.peers.keepalive.messages.Pong;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
@ -36,6 +41,10 @@ import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
@ -45,16 +54,20 @@ import java.security.SignatureException;
import java.math.BigInteger;
import java.util.Random;
import java.util.UUID;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import static org.bitcoinj.core.Utils.HEX;
public class PrivateNotificationManager {
public class PrivateNotificationManager implements MessageListener {
private static final Logger log = LoggerFactory.getLogger(PrivateNotificationManager.class);
private final P2PService p2PService;
@ -68,6 +81,8 @@ public class PrivateNotificationManager {
private ECKey privateNotificationSigningKey;
@Nullable
private PrivateNotificationMessage privateNotificationMessage;
private final NetworkNode networkNode;
private Consumer<String> pingResponseHandler = null;
///////////////////////////////////////////////////////////////////////////////////////////
@ -76,11 +91,13 @@ public class PrivateNotificationManager {
@Inject
public PrivateNotificationManager(P2PService p2PService,
NetworkNode networkNode,
MailboxMessageService mailboxMessageService,
KeyRing keyRing,
@Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
this.p2PService = p2PService;
this.networkNode = networkNode;
this.mailboxMessageService = mailboxMessageService;
this.keyRing = keyRing;
@ -173,5 +190,38 @@ public class PrivateNotificationManager {
}
}
public void sendPing(NodeAddress peersNodeAddress, Consumer<String> resultHandler) {
Ping ping = new Ping(new Random().nextInt(), 0);
log.info("Send Ping to peer {}, nonce={}", peersNodeAddress, ping.getNonce());
SettableFuture<Connection> future = networkNode.sendMessage(peersNodeAddress, ping);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Connection connection) {
connection.addMessageListener(PrivateNotificationManager.this);
pingResponseHandler = resultHandler;
}
@Override
public void onFailure(@NotNull Throwable throwable) {
String errorMessage = "Sending ping to " + peersNodeAddress.getHostNameForDisplay() +
" failed. That is expected if the peer is offline.\n\tping=" + ping +
".\n\tException=" + throwable.getMessage();
log.info(errorMessage);
resultHandler.accept(errorMessage);
}
}, MoreExecutors.directExecutor());
}
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof Pong) {
Pong pong = (Pong) networkEnvelope;
String key = connection.getPeersNodeAddressOptional().get().getFullAddress();
log.info("Received Pong! {} from {}", pong, key);
connection.removeMessageListener(this);
if (pingResponseHandler != null) {
pingResponseHandler.accept("SUCCESS");
}
}
}
}

View file

@ -96,7 +96,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported",
lastVersion, Version.VERSION));
bisqSetup.setTorAddressUpgradeHandler(() -> log.info("setTorAddressUpgradeHandler"));
corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));
tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("Error taking offer: " + errorMessage));
}

View file

@ -22,6 +22,7 @@ import bisq.core.account.sign.SignedWitnessStorageService;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.Alert;
import bisq.core.alert.AlertManager;
import bisq.core.alert.PrivateNotificationManager;
import bisq.core.alert.PrivateNotificationPayload;
import bisq.core.api.CoreMoneroNodeService;
import bisq.core.btc.model.AddressEntry;
@ -37,6 +38,10 @@ import bisq.core.payment.AmazonGiftCardAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.trade.TradeManager;
import bisq.core.trade.TradeTxException;
import bisq.core.user.Preferences;
@ -45,8 +50,10 @@ import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.network.utils.Utils;
import bisq.common.Timer;
import bisq.common.UserThread;
@ -86,6 +93,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -106,19 +115,6 @@ public class HavenoSetup {
private static final String VERSION_FILE_NAME = "version";
private static final String RESYNC_SPV_FILE_NAME = "resyncSpv";
public interface HavenoSetupListener {
default void onInitP2pNetwork() {
}
default void onInitWallet() {
}
default void onRequestWalletPassword() {
}
void onSetupComplete();
}
private static final long STARTUP_TIMEOUT_MINUTES = 4;
private final DomainInitialisation domainInitialisation;
@ -129,6 +125,7 @@ public class HavenoSetup {
private final BtcWalletService btcWalletService;
private final XmrWalletService xmrWalletService;
private final P2PService p2PService;
private final PrivateNotificationManager privateNotificationManager;
private final SignedWitnessStorageService signedWitnessStorageService;
private final TradeManager tradeManager;
private final OpenOfferManager openOfferManager;
@ -141,7 +138,9 @@ public class HavenoSetup {
private final CoinFormatter formatter;
private final LocalBitcoinNode localBitcoinNode;
private final AppStartupState appStartupState;
private final MediationManager mediationManager;
private final RefundManager refundManager;
private final ArbitrationManager arbitrationManager;
@Setter
@Nullable
private Consumer<Runnable> displayTacHandler;
@ -188,8 +187,10 @@ public class HavenoSetup {
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Runnable torAddressUpgradeHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
private BooleanProperty p2pNetworkReady;
@ -199,6 +200,19 @@ public class HavenoSetup {
private MonadicBinding<Boolean> p2pNetworkAndWalletInitialized;
private final List<HavenoSetupListener> havenoSetupListeners = new ArrayList<>();
public interface HavenoSetupListener {
default void onInitP2pNetwork() {
}
default void onInitWallet() {
}
default void onRequestWalletPassword() {
}
void onSetupComplete();
}
@Inject
public HavenoSetup(DomainInitialisation domainInitialisation,
P2PNetworkSetup p2PNetworkSetup,
@ -208,6 +222,7 @@ public class HavenoSetup {
XmrWalletService xmrWalletService,
BtcWalletService btcWalletService,
P2PService p2PService,
PrivateNotificationManager privateNotificationManager,
SignedWitnessStorageService signedWitnessStorageService,
TradeManager tradeManager,
OpenOfferManager openOfferManager,
@ -220,7 +235,10 @@ public class HavenoSetup {
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
LocalBitcoinNode localBitcoinNode,
AppStartupState appStartupState,
Socks5ProxyProvider socks5ProxyProvider) {
Socks5ProxyProvider socks5ProxyProvider,
MediationManager mediationManager,
RefundManager refundManager,
ArbitrationManager arbitrationManager) {
this.domainInitialisation = domainInitialisation;
this.p2PNetworkSetup = p2PNetworkSetup;
this.walletAppSetup = walletAppSetup;
@ -229,6 +247,7 @@ public class HavenoSetup {
this.xmrWalletService = xmrWalletService;
this.btcWalletService = btcWalletService;
this.p2PService = p2PService;
this.privateNotificationManager = privateNotificationManager;
this.signedWitnessStorageService = signedWitnessStorageService;
this.tradeManager = tradeManager;
this.openOfferManager = openOfferManager;
@ -241,6 +260,9 @@ public class HavenoSetup {
this.formatter = formatter;
this.localBitcoinNode = localBitcoinNode;
this.appStartupState = appStartupState;
this.mediationManager = mediationManager;
this.refundManager = refundManager;
this.arbitrationManager = arbitrationManager;
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
}
@ -313,6 +335,8 @@ public class HavenoSetup {
maybeShowSecurityRecommendation();
maybeShowLocalhostRunningInfo();
maybeShowAccountSigningStateInfo();
maybeShowTorAddressUpgradeInformation();
checkInboundConnections();
}
@ -663,6 +687,37 @@ public class HavenoSetup {
}
}
/**
* Check if we have inbound connections. If not, try to ping ourselves.
* If Haveno cannot connect to its own onion address through Tor, display
* an informative message to let the user know to configure their firewall else
* their offers will not be reachable.
* Repeat this test hourly.
*/
private void checkInboundConnections() {
NodeAddress onionAddress = p2PService.getNetworkNode().nodeAddressProperty().get();
if (onionAddress == null || !onionAddress.getFullAddress().contains("onion")) {
return;
}
if (p2PService.getNetworkNode().upTime() > TimeUnit.HOURS.toMillis(1) &&
p2PService.getNetworkNode().getInboundConnectionCount() == 0) {
// we've been online a while and did not find any inbound connections; lets try the self-ping check
log.info("no recent inbound connections found, starting the self-ping test");
privateNotificationManager.sendPing(onionAddress, stringResult -> {
log.info(stringResult);
if (stringResult.contains("failed")) {
getP2PNetworkStatusIconId().set("flashing:image-yellow_circle");
}
});
}
// schedule another inbound connection check for later
int nextCheckInMinutes = 30 + new Random().nextInt(30);
log.debug("next inbound connections check in {} minutes", nextCheckInMinutes);
UserThread.runAfter(this::checkInboundConnections, nextCheckInMinutes, TimeUnit.MINUTES);
}
private void maybeShowSecurityRecommendation() {
if (user.getPaymentAccountsAsObservable() == null) return;
String key = "remindPasswordAndBackup";
@ -733,6 +788,30 @@ public class HavenoSetup {
}
}
private void maybeShowTorAddressUpgradeInformation() {
if (Config.baseCurrencyNetwork().isTestnet() ||
Utils.isV3Address(Objects.requireNonNull(p2PService.getNetworkNode().getNodeAddress()).getHostName())) {
return;
}
maybeRunTorNodeAddressUpgradeHandler();
tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> {
long numPendingTrades = (long) newValue;
if (numPendingTrades == 0) {
maybeRunTorNodeAddressUpgradeHandler();
}
});
}
private void maybeRunTorNodeAddressUpgradeHandler() {
if (mediationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) &&
refundManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) &&
arbitrationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) &&
tradeManager.getNumPendingTrades().isEqualTo(0).get()) {
Objects.requireNonNull(torAddressUpgradeHandler).run();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
@ -776,6 +855,10 @@ public class HavenoSetup {
return p2PNetworkSetup.getP2PNetworkIconId();
}
public StringProperty getP2PNetworkStatusIconId() {
return p2PNetworkSetup.getP2PNetworkStatusIconId();
}
public BooleanProperty getUpdatedDataReceived() {
return p2PNetworkSetup.getUpdatedDataReceived();
}

View file

@ -63,6 +63,8 @@ public class P2PNetworkSetup {
@Getter
final StringProperty p2PNetworkIconId = new SimpleStringProperty();
@Getter
final StringProperty p2PNetworkStatusIconId = new SimpleStringProperty();
@Getter
final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true);
@Getter
final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane");
@ -118,10 +120,12 @@ public class P2PNetworkSetup {
p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() {
@Override
public void onConnection(Connection connection) {
updateNetworkStatusIndicator();
}
@Override
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
updateNetworkStatusIndicator();
// We only check at seed nodes as they are running the latest version
// Other disconnects might be caused by peers running an older version
if (connection.getConnectionState().isSeedNode() &&
@ -225,4 +229,14 @@ public class P2PNetworkSetup {
public void setSplashP2PNetworkAnimationVisible(boolean value) {
splashP2PNetworkAnimationVisible.set(value);
}
private void updateNetworkStatusIndicator() {
if (p2PService.getNetworkNode().getInboundConnectionCount() > 0) {
p2PNetworkStatusIconId.set("image-green_circle");
} else if (p2PService.getNetworkNode().getOutboundConnectionCount() > 0) {
p2PNetworkStatusIconId.set("image-yellow_circle");
} else {
p2PNetworkStatusIconId.set("image-alert-round");
}
}
}