From 0ddd333ce6d833332b348d3706c3ff21c0ab9543 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Sat, 7 Nov 2020 23:09:11 -0600 Subject: [PATCH] Don't allow trade start if BitcoinJ is not fully synced Adds a check that chain height is within 3 blocks of the reported height from bitcoin peers -> if not the user cannot take an offer or have an existing offer taken. It shows a message informing the user that Bisq is not currently synced, advising them to do an SPV resync if necessary. Additionally under Settings/Network a field has been added to show the chain height of Bisq vs the Peer group. Added after discussion with chimp1984: - Correct initialization of chainHeight property - Rename "Latest BTC block height" display field for clarity - Enforce chain sync rule for Take Offer scenario - Enforce chain synch rule for Check offer availability scenario - change method name to be clearer --- .../bisq/core/btc/setup/WalletsSetup.java | 11 ++++++++++ .../bisq/core/btc/wallet/WalletService.java | 4 ++++ .../bisq/core/offer/OpenOfferManager.java | 8 +++++++ .../resources/i18n/displayStrings.properties | 4 ++++ .../offer/offerbook/OfferBookViewModel.java | 5 +++++ .../settings/network/NetworkSettingsView.fxml | 7 ++++-- .../settings/network/NetworkSettingsView.java | 8 ++++++- .../main/java/bisq/desktop/util/GUIUtil.java | 9 ++++++++ .../offerbook/OfferBookViewModelTest.java | 22 +++++++++---------- 9 files changed, 64 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java index 9b1ce934186..46e43c1bca0 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java @@ -242,6 +242,7 @@ protected void onSetupCompleted() { return message; }); + chainHeight.set(chain.getBestChainHeight()); chain.addNewBestBlockListener(block -> { connectedPeers.set(peerGroup.getConnectedPeers()); chainHeight.set(block.getHeight()); @@ -522,6 +523,16 @@ public boolean isDownloadComplete() { return downloadPercentageProperty().get() == 1d; } + public boolean isChainHeightSyncedWithinTolerance() { + int peersChainHeight = PeerGroup.getMostCommonChainHeight(connectedPeers.get()); + int bestChainHeight = walletConfig.chain().getBestChainHeight(); + if (peersChainHeight - bestChainHeight <= 3) { + return true; + } + log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), peersChainHeight); + return false; + } + public Set
getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return addressEntryList.getAddressEntriesAsListImmutable().stream() .filter(addressEntry -> addressEntry.getContext() == context) diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index 251aac97172..b5ac6118bdb 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -558,6 +558,10 @@ public int getBestChainHeight() { return isWalletReady() && chain != null ? chain.getBestChainHeight() : 0; } + public boolean isChainHeightSyncedWithinTolerance() { + return walletsSetup.isChainHeightSyncedWithinTolerance(); + } + public Transaction getClonedTransaction(Transaction tx) { return new Transaction(params, tx.bitcoinSerialize()); } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index c06b6d11fd2..7db70114563 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -580,6 +580,14 @@ private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, No return; } + // Don't allow trade start if BitcoinJ is not fully synced (bisq issue #4764) + if (!btcWalletService.isChainHeightSyncedWithinTolerance()) { + errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced."; + log.info(errorMessage); + sendAckMessage(request, peer, false, errorMessage); + return; + } + if (stopped) { errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; log.debug(errorMessage); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 6d011b5e76c..ff22131e93e 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1275,6 +1275,7 @@ settings.net.creationDateColumn=Established settings.net.connectionTypeColumn=In/Out settings.net.sentDataLabel=Sent data statistics settings.net.receivedDataLabel=Received data statistics +settings.net.chainHeightLabel=Latest BTC block height settings.net.roundTripTimeColumn=Roundtrip settings.net.sentBytesColumn=Sent settings.net.receivedBytesColumn=Received @@ -1289,6 +1290,7 @@ settings.net.needRestart=You need to restart the application to apply that chang settings.net.notKnownYet=Not known yet... settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec +settings.net.chainHeight=Bisq: {0} | Peers: {1} settings.net.ips=[IP address:port | host name:port | onion address:port] (comma separated). Port can be omitted if default is used (8333). settings.net.seedNode=Seed node settings.net.directPeer=Peer (direct) @@ -2781,6 +2783,8 @@ popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=You need to wait until you are fully connected to the network.\nThat might take up to about 2 minutes at startup. popup.warning.notSufficientConnectionsToBtcNetwork=You need to wait until you have at least {0} connections to the Bitcoin network. popup.warning.downloadNotComplete=You need to wait until the download of missing Bitcoin blocks is complete. +popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\n\ + You can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file] popup.warning.removeOffer=Are you sure you want to remove that offer?\nThe maker fee of {0} will be lost if you remove that offer. popup.warning.tooLargePercentageValue=You cannot set a percentage of 100% or larger. popup.warning.examplePercentageValue=Please enter a percentage number like \"5.4\" for 5.4% diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index 1df383ea071..a6cd541f81e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -26,6 +26,7 @@ import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.setup.WalletsSetup; import bisq.core.filter.FilterManager; import bisq.core.locale.BankUtil; import bisq.core.locale.CountryUtil; @@ -99,6 +100,7 @@ class OfferBookViewModel extends ActivatableViewModel { private final User user; private final OfferBook offerBook; final Preferences preferences; + private final WalletsSetup walletsSetup; private final P2PService p2PService; final PriceFeedService priceFeedService; private final ClosedTradableManager closedTradableManager; @@ -142,6 +144,7 @@ public OfferBookViewModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences, + WalletsSetup walletsSetup, P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, @@ -156,6 +159,7 @@ public OfferBookViewModel(User user, this.user = user; this.offerBook = offerBook; this.preferences = preferences; + this.walletsSetup = walletsSetup; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.closedTradableManager = closedTradableManager; @@ -532,6 +536,7 @@ boolean hasPaymentAccountForCurrency() { boolean canCreateOrTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && + GUIUtil.isChainHeightSyncedWithinToleranceOrShowPopup(walletsSetup) && GUIUtil.isBootstrappedOrShowPopup(p2PService); } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.fxml b/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.fxml index 5b966f09736..eae359dec22 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.fxml @@ -94,7 +94,7 @@ - + @@ -159,7 +159,10 @@ - + + + diff --git a/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.java index dd1d53be109..d328a91d25d 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/network/NetworkSettingsView.java @@ -45,6 +45,8 @@ import bisq.common.ClockWatcher; import bisq.common.UserThread; +import org.bitcoinj.core.PeerGroup; + import javax.inject.Inject; import javafx.fxml.FXML; @@ -86,7 +88,7 @@ public class NetworkSettingsView extends ActivatableView { @FXML InputTextField btcNodesInputTextField; @FXML - TextField onionAddress, sentDataTextField, receivedDataTextField; + TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; @FXML Label p2PPeersLabel, bitcoinPeersLabel; @FXML @@ -180,6 +182,7 @@ public void initialize() { connectionTypeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.connectionTypeColumn"))); sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); + chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); @@ -487,6 +490,9 @@ private void updateBitcoinPeersTable() { bitcoinNetworkListItems.setAll(walletsSetup.getPeerGroup().getConnectedPeers().stream() .map(BitcoinNetworkListItem::new) .collect(Collectors.toList())); + chainHeightTextField.textProperty().setValue(Res.get("settings.net.chainHeight", + walletsSetup.chainHeightProperty().get(), + PeerGroup.getMostCommonChainHeight(walletsSetup.connectedPeersProperty().get()))); } } diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index e90559cd1d0..2ecb9ee2423 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -756,6 +756,15 @@ public static boolean isReadyForTxBroadcastOrShowPopup(P2PService p2PService, Wa return true; } + public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(WalletsSetup walletsSetup) { + if (!walletsSetup.isChainHeightSyncedWithinTolerance()) { + new Popup().information(Res.get("popup.warning.chainNotSynced")).show(); + return false; + } + + return true; + } + public static boolean canCreateOrTakeOfferOrShowPopup(User user, Navigation navigation) { if (!user.hasAcceptedRefundAgents()) { new Popup().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 3b76f7e2742..9cd74e457fb 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -229,7 +229,7 @@ public void testMaxCharactersForAmountWithNoOffes() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForAmount.intValue()); } @@ -243,7 +243,7 @@ public void testMaxCharactersForAmount() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -261,7 +261,7 @@ public void testMaxCharactersForAmountRange() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -280,7 +280,7 @@ public void testMaxCharactersForVolumeWithNoOffes() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForVolume.intValue()); } @@ -294,7 +294,7 @@ public void testMaxCharactersForVolume() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -312,7 +312,7 @@ public void testMaxCharactersForVolumeRange() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -331,7 +331,7 @@ public void testMaxCharactersForPriceWithNoOffers() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForPrice.intValue()); } @@ -345,7 +345,7 @@ public void testMaxCharactersForPrice() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -363,7 +363,7 @@ public void testMaxCharactersForPriceDistanceWithNoOffers() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); - final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @@ -391,7 +391,7 @@ public void testMaxCharactersForPriceDistance() { item4.getOffer().setPriceFeedService(priceFeedService); offerBookListItems.addAll(item1, item2); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, priceFeedService, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService, null, null, null, null, coinFormatter, new BsqFormatter()); model.activate(); @@ -412,7 +412,7 @@ public void testGetPrice() { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); - final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, + final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, null, null, null, null, coinFormatter, new BsqFormatter()); final OfferBookListItem item = make(btcBuyItem.but(