From 894d2be5dbc3fd139a2a7d7f6911a903d4123479 Mon Sep 17 00:00:00 2001 From: Steven Barclay Date: Thu, 20 Aug 2020 17:51:57 +0800 Subject: [PATCH 01/52] Rename misleading OfferBookChartViewModel method name s/isAnyPricePresent/isAnyPriceAbsent/g Also use a lambda to shorten 'currenciesUpdatedListener' slightly. --- .../offerbook/OfferBookChartViewModel.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java index dc6fcf4de3..faa0245573 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -50,7 +50,6 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -127,14 +126,12 @@ class OfferBookChartViewModel extends ActivatableViewModel { fillTradeCurrencies(); }; - currenciesUpdatedListener = new ChangeListener<>() { - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if (!isAnyPricePresent()) { - offerBook.fillOfferBookListItems(); - updateChartData(); - priceFeedService.updateCounterProperty().removeListener(currenciesUpdatedListener); - } + currenciesUpdatedListener = (observable, oldValue, newValue) -> { + if (!isAnyPriceAbsent()) { + offerBook.fillOfferBookListItems(); + updateChartData(); + var self = this; + priceFeedService.updateCounterProperty().removeListener(self.currenciesUpdatedListener); } }; @@ -163,7 +160,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { fillTradeCurrencies(); updateChartData(); - if (isAnyPricePresent()) + if (isAnyPriceAbsent()) priceFeedService.updateCounterProperty().addListener(currenciesUpdatedListener); syncPriceFeedCurrency(); @@ -271,7 +268,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { priceFeedService.setCurrencyCode(getCurrencyCode()); } - private boolean isAnyPricePresent() { + private boolean isAnyPriceAbsent() { return offerBookListItems.stream().anyMatch(item -> item.getOffer().getPrice() == null); } @@ -291,11 +288,11 @@ class OfferBookChartViewModel extends ActivatableViewModel { Comparator offerAmountComparator = Comparator.comparing(Offer::getAmount).reversed(); var buyOfferSortComparator = - offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. - .thenComparing(offerAmountComparator); + offerPriceComparator.reversed() // Buy offers, as opposed to sell offers, are primarily sorted from high price to low. + .thenComparing(offerAmountComparator); var sellOfferSortComparator = - offerPriceComparator - .thenComparing(offerAmountComparator); + offerPriceComparator + .thenComparing(offerAmountComparator); List allBuyOffers = offerBookListItems.stream() .map(OfferBookListItem::getOffer) From 23688db18c035f1f43692696425ca9edb2676917 Mon Sep 17 00:00:00 2001 From: Steven Barclay Date: Thu, 20 Aug 2020 18:40:13 +0800 Subject: [PATCH 02/52] Fix intermittent blank price cells in offer book view Replace faulty cell update logic, which uses a ChangeListener, added in July 2017 (#73f21399) to keep the price column in the offer book table up to date, as it appears to occasionally result in blank cells. Also it seems only the prices, not the volumes, were being kept in sync with the market price feed. Make the price and volume cells stateless and keep them in sync with the market feed by adding it as a dependency of each OfferBookListItem Observable generated by the cell value factory, instead of directly attaching listeners to it. In this way, TableCell::updateItem will be called by the framework whenever the price/volume needs updating. (This does have the disadvantage that if the price feed is unavailable, causing Offer::getPrice to return null, then the cells will reflect that immediately instead of showing any old, stale values, but that is necessary for the UI to behave consistently anyway.) --- .../main/offer/offerbook/OfferBookView.java | 74 +++---------------- 1 file changed, 11 insertions(+), 63 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 40ebfc78b7..5753c14c0b 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -71,7 +71,6 @@ import javax.inject.Named; import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; -import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.control.ComboBox; import javafx.scene.control.ContentDisplay; @@ -731,6 +730,12 @@ public class OfferBookView extends ActivatableViewAndModel asPriceDependentObservable(OfferBookListItem item) { + return item.getOffer().isUseMarketBasedPrice() + ? EasyBind.map(model.priceFeedService.updateCounterProperty(), n -> item) + : new ReadOnlyObjectWrapper<>(item); + } + private AutoTooltipTableColumn getPriceColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>("") { { @@ -738,59 +743,20 @@ public class OfferBookView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { - private OfferBookListItem offerBookListItem; - private ChangeListener priceChangedListener; - ChangeListener sceneChangeListener; - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - if (getTableView().getScene() != null && sceneChangeListener == null) { - sceneChangeListener = (observable, oldValue, newValue) -> { - if (newValue == null) { - if (priceChangedListener != null) { - model.priceFeedService.updateCounterProperty().removeListener(priceChangedListener); - priceChangedListener = null; - } - offerBookListItem = null; - setGraphic(null); - getTableView().sceneProperty().removeListener(sceneChangeListener); - sceneChangeListener = null; - } - }; - getTableView().sceneProperty().addListener(sceneChangeListener); - } - - this.offerBookListItem = item; - - if (priceChangedListener == null) { - priceChangedListener = (observable, oldValue, newValue) -> { - if (offerBookListItem != null && offerBookListItem.getOffer().getPrice() != null) { - setGraphic(getPriceLabel(model.getPrice(offerBookListItem), offerBookListItem)); - } - }; - model.priceFeedService.updateCounterProperty().addListener(priceChangedListener); - } - setGraphic(getPriceLabel(item.getOffer().getPrice() == null ? Res.get("shared.na") : model.getPrice(item), item)); + setGraphic(getPriceLabel(model.getPrice(item), item)); } else { - if (priceChangedListener != null) { - model.priceFeedService.updateCounterProperty().removeListener(priceChangedListener); - priceChangedListener = null; - } - if (sceneChangeListener != null) { - getTableView().sceneProperty().removeListener(sceneChangeListener); - sceneChangeListener = null; - } - this.offerBookListItem = null; setGraphic(null); } } @@ -845,35 +811,19 @@ public class OfferBookView extends ActivatableViewAndModel new ReadOnlyObjectWrapper<>(offer.getValue())); + column.setCellValueFactory(offer -> asPriceDependentObservable(offer.getValue())); column.setCellFactory( new Callback<>() { @Override public TableCell call( TableColumn column) { return new TableCell<>() { - private OfferBookListItem offerBookListItem; - final ChangeListener listener = new ChangeListener<>() { - @Override - public void changed(ObservableValue observable, - Number oldValue, - Number newValue) { - if (offerBookListItem != null && offerBookListItem.getOffer().getVolume() != null) { - setText(""); - setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(offerBookListItem), - model.getNumberOfDecimalsForVolume(offerBookListItem))); - model.priceFeedService.updateCounterProperty().removeListener(listener); - } - } - }; - @Override public void updateItem(final OfferBookListItem item, boolean empty) { super.updateItem(item, empty); + if (item != null && !empty) { if (item.getOffer().getPrice() == null) { - this.offerBookListItem = item; - model.priceFeedService.updateCounterProperty().addListener(listener); setText(Res.get("shared.na")); setGraphic(null); } else { @@ -882,8 +832,6 @@ public class OfferBookView extends ActivatableViewAndModel tableRow = getTableRow(); if (newItem != null && !empty) { final Offer offer = newItem.getOffer(); boolean myOffer = model.isMyOffer(offer); From badc872533b9fe04b89a1349e7358595dbec634a Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 24 Aug 2020 00:13:14 -0500 Subject: [PATCH 03/52] Refactoring --- .../network/p2p/peers/BroadcastHandler.java | 229 ++++++++++-------- .../bisq/network/p2p/peers/Broadcaster.java | 6 +- 2 files changed, 130 insertions(+), 105 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index cc8469d8bd..facdba5eef 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -45,7 +45,8 @@ import org.jetbrains.annotations.Nullable; @Slf4j public class BroadcastHandler implements PeerManager.Listener { - private static final long TIMEOUT = 60; + // TODO check if that is not too low + private static final long BASE_TIMEOUT_MS = 60; /////////////////////////////////////////////////////////////////////////////////////////// @@ -59,14 +60,12 @@ public class BroadcastHandler implements PeerManager.Listener { } public interface Listener { - @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts); void onBroadcastedToFirstPeer(BroadcastMessage message); void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts); - @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) void onBroadcastFailed(String errorMessage); } @@ -76,7 +75,7 @@ public class BroadcastHandler implements PeerManager.Listener { /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; - public final String uid; + private final String uid; private final PeerManager peerManager; private boolean stopped = false; private int numOfCompletedBroadcasts = 0; @@ -93,11 +92,12 @@ public class BroadcastHandler implements PeerManager.Listener { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) { + BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) { this.networkNode = networkNode; this.peerManager = peerManager; - peerManager.addListener(this); uid = UUID.randomUUID().toString(); + + peerManager.addListener(this); } public void cancel() { @@ -110,7 +110,9 @@ public class BroadcastHandler implements PeerManager.Listener { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, ResultHandler resultHandler, + public void broadcast(BroadcastMessage message, + @Nullable NodeAddress sender, + ResultHandler resultHandler, @Nullable Listener listener) { this.message = message; this.resultHandler = resultHandler; @@ -118,102 +120,128 @@ public class BroadcastHandler implements PeerManager.Listener { Set connectedPeersSet = networkNode.getConfirmedConnections() .stream() - .filter(connection -> !connection.getPeersNodeAddressOptional().get().equals(sender)) + .filter(connection -> connection.getPeersNodeAddressOptional().isPresent() && + !connection.getPeersNodeAddressOptional().get().equals(sender)) // We don't broadcast back to sender .collect(Collectors.toSet()); - if (!connectedPeersSet.isEmpty()) { - numOfCompletedBroadcasts = 0; - List connectedPeersList = new ArrayList<>(connectedPeersSet); - Collections.shuffle(connectedPeersList); - numPeers = connectedPeersList.size(); - int delay = 50; - - boolean isDataOwner = (sender != null) && sender.equals(networkNode.getNodeAddress()); - if (!isDataOwner) { - // for not data owner (relay nodes) we send to max. 7 nodes and use a longer delay - numPeers = Math.min(7, connectedPeersList.size()); - delay = 100; - } - - long timeoutDelay = TIMEOUT + delay * numPeers; - timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions - String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec."; - - log.debug(errorMessage + "\n\t" + - "numOfPeers=" + numPeers + "\n\t" + - "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" + - "numOfFailedBroadcasts=" + numOfFailedBroadcasts); - onFault(errorMessage, false); - }, timeoutDelay); - - log.debug("Broadcast message to {} peers out of {} total connected peers.", numPeers, connectedPeersSet.size()); - for (int i = 0; i < numPeers; i++) { - if (stopped) - break; // do not continue sending after a timeout or a cancellation - - final long minDelay = (i + 1) * delay; - final long maxDelay = (i + 2) * delay; - final Connection connection = connectedPeersList.get(i); - UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS); - } - } else { + if (connectedPeersSet.isEmpty()) { onFault("Message not broadcasted because we have no available peers yet.\n\t" + "message = " + Utilities.toTruncatedString(message), false); + return; + } + + numOfCompletedBroadcasts = 0; + + List connectedPeersList = new ArrayList<>(connectedPeersSet); + Collections.shuffle(connectedPeersList); + int delay; + + // If we are the owner of the data we broadcast faster and with higher resilience + boolean isDataOwner = sender != null && sender.equals(networkNode.getNodeAddress()); + if (isDataOwner) { + numPeers = connectedPeersList.size(); + delay = 50; + } else { + // Relay nodes do not broadcast to all connected nodes + numPeers = Math.min(7, connectedPeersList.size()); + delay = 100; + } + + long timeoutDelay = BASE_TIMEOUT_MS + delay * numPeers; + timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec."; + + log.warn(errorMessage + "\n\t" + + "numOfPeers=" + numPeers + "\n\t" + + "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" + + "numOfFailedBroadcasts=" + numOfFailedBroadcasts); + onFault(errorMessage, true); + }, timeoutDelay); + + log.debug("Broadcast message to {} peers out of {} total connected peers.", numPeers, connectedPeersSet.size()); + for (int i = 0; i < numPeers; i++) { + if (stopped) + break; // Do not continue sending after a timeout or a cancellation + + long minDelay = (i + 1) * delay; + long maxDelay = (i + 2) * delay; + Connection connection = connectedPeersList.get(i); + UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS); } } private void sendToPeer(Connection connection, BroadcastMessage message) { String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" + "message = " + Utilities.toTruncatedString(message); - if (!stopped) { - if (!connection.isStopped()) { - if (connection.noCapabilityRequiredOrCapabilityIsSupported(message)) { - NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); - SettableFuture future = networkNode.sendMessage(connection, message); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(Connection connection) { - numOfCompletedBroadcasts++; - if (!stopped) { - if (listener != null) - listener.onBroadcasted(message, numOfCompletedBroadcasts); - if (listener != null && numOfCompletedBroadcasts == 1) - listener.onBroadcastedToFirstPeer(message); - - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { - if (listener != null) - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); - - cleanup(); - resultHandler.onCompleted(BroadcastHandler.this); - } - } else { - // TODO investigate why that is called very often at seed nodes - onFault("stopped at onSuccess: " + errorMessage, false); - } - } - - @Override - public void onFailure(@NotNull Throwable throwable) { - numOfFailedBroadcasts++; - if (!stopped) { - log.info("Broadcast to " + nodeAddress + " failed.\n\t" + - "ErrorMessage=" + throwable.getMessage()); - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) - onFault("stopped at onFailure: " + errorMessage); - } else { - onFault("stopped at onFailure: " + errorMessage); - } - } - }); - } - } else { - onFault("Connection stopped already", false); - } - } else { - onFault("stopped at sendToPeer: " + errorMessage, false); + if (stopped) { + onFault("Handler stopped already: " + errorMessage, false); + return; } + + if (connection.isStopped()) { + onFault("Connection stopped already.", false); + return; + } + + if (!connection.noCapabilityRequiredOrCapabilityIsSupported(message)) { + onFault("Peer does not support requires capability.", false); + return; + } + + if (!connection.getPeersNodeAddressOptional().isPresent()) { + onFault("Peer node address is not present.", false); + return; + } + + NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); + SettableFuture future = networkNode.sendMessage(connection, message); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + numOfCompletedBroadcasts++; + + if (stopped) { + // TODO investigate why that is called very often at seed nodes + onFault("stopped at onSuccess: " + errorMessage, false); + return; + } + + if (listener != null) { + listener.onBroadcasted(message, numOfCompletedBroadcasts); + + if (numOfCompletedBroadcasts == 1) { + listener.onBroadcastedToFirstPeer(message); + } + } + + + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { + if (listener != null) { + listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); + } + + cleanup(); + resultHandler.onCompleted(BroadcastHandler.this); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + numOfFailedBroadcasts++; + + if (stopped) { + onFault("stopped at onFailure: " + errorMessage, true); + return; + } + + log.warn("Broadcast to {} failed. ErrorMessage={}", nodeAddress, throwable.getMessage()); + + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { + onFault("Last broadcast cause a failure: " + errorMessage, true); + } + } + }); } @@ -248,23 +276,20 @@ public class BroadcastHandler implements PeerManager.Listener { } } - private void onFault(String errorMessage) { - onFault(errorMessage, true); - } - private void onFault(String errorMessage, boolean logWarning) { cleanup(); - if (logWarning) + if (logWarning) { log.warn(errorMessage); - else - log.debug(errorMessage); + } - if (listener != null) + if (listener != null) { listener.onBroadcastFailed(errorMessage); - if (listener != null && (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers || stopped)) - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers || stopped) { + listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); + } + } resultHandler.onFault(this); } @@ -282,6 +307,6 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public int hashCode() { - return uid != null ? uid.hashCode() : 0; + return uid.hashCode(); } } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 727c488b0e..9b004583b5 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -31,7 +31,6 @@ import org.jetbrains.annotations.Nullable; public class Broadcaster implements BroadcastHandler.ResultHandler { private final NetworkNode networkNode; private final PeerManager peerManager; - private final Set broadcastHandlers = new CopyOnWriteArraySet<>(); @@ -46,7 +45,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { } public void shutDown() { - broadcastHandlers.stream().forEach(BroadcastHandler::cancel); + broadcastHandlers.forEach(BroadcastHandler::cancel); broadcastHandlers.clear(); } @@ -55,7 +54,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, + public void broadcast(BroadcastMessage message, + @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager); broadcastHandler.broadcast(message, sender, this, listener); From c4932829fe82464aa35a2eb9d0ddbf0157c7647d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 24 Aug 2020 00:37:14 -0500 Subject: [PATCH 04/52] Refactoring --- .../core/dao/monitoring/network/StateNetworkService.java | 2 +- .../dao/node/full/network/FullNodeNetworkService.java | 2 +- .../dao/node/lite/network/LiteNodeNetworkService.java | 2 +- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 5 +++++ .../java/bisq/network/p2p/storage/P2PDataStorage.java | 8 ++++---- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java index 69d6c7ea63..8300c76b23 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java @@ -156,7 +156,7 @@ public abstract class StateNetworkService listener.onNewBlockReceived(newBlockBroadcastMessage)); } else { log.debug("We had that message already and do not further broadcast it. extBlockId={}", extBlockId); diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 9b004583b5..919d64543b 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -54,6 +54,11 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { // API /////////////////////////////////////////////////////////////////////////////////////////// + public void broadcast(BroadcastMessage message, + @Nullable NodeAddress sender) { + broadcast(message, sender, null); + } + public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { diff --git a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java index 34e29f3f5f..2283547a1f 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -529,7 +529,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers // Broadcast the payload if requested by caller if (allowBroadcast) - broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender, null); + broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender); return true; } @@ -675,7 +675,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000); // Always broadcast refreshes - broadcaster.broadcast(refreshTTLMessage, sender, null); + broadcaster.broadcast(refreshTTLMessage, sender); return true; } @@ -725,9 +725,9 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers printData("after remove"); if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { - broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null); + broadcaster.broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender); } else { - broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender, null); + broadcaster.broadcast(new RemoveDataMessage(protectedStorageEntry), sender); } return true; From ac3c6e07f054e57190abd872c5ec6dd9c83e5895 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 25 Aug 2020 11:29:27 -0300 Subject: [PATCH 05/52] Fix setUpScaffold() signature Adds the missing String[] params to the method signature, so test cases can pass any needed combination of options to the scaffolding setup from a @BeforeAll method. --- apitest/src/test/java/bisq/apitest/ApiTestCase.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 286e7f8c20..b962a2f3aa 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -73,9 +73,9 @@ public class ApiTestCase { grpcStubs = new GrpcStubs(alicedaemon, config).init(); } - public static void setUpScaffold() + public static void setUpScaffold(String[] params) throws InterruptedException, ExecutionException, IOException { - scaffold = new Scaffold(new String[]{}).setUp(); + scaffold = new Scaffold(params).setUp(); config = scaffold.config; grpcStubs = new GrpcStubs(alicedaemon, config).init(); } From 2c803ef8119ca4ba0ec98ad5de927b9c3cea0143 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 25 Aug 2020 11:42:09 -0300 Subject: [PATCH 06/52] Move :cli test.sh to :apitest mainnet-test.sh * The bats test script was moved to the apitest subproject and renamed. * Version tests were updated for release 1.3.7. * The duplicated "test getoffers buy eur check return status" was replaced by a new "test getoffers sell eur check return status" test. * The bats dependency was switched to bats-core because development has halted on https://github.com/sstephenson/bats/tree/master. The new bats repository is https://github.com/bats-core/bats-core/tree/master --- cli/test.sh => apitest/scripts/mainnet-test.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename cli/test.sh => apitest/scripts/mainnet-test.sh (94%) diff --git a/cli/test.sh b/apitest/scripts/mainnet-test.sh similarity index 94% rename from cli/test.sh rename to apitest/scripts/mainnet-test.sh index 9878d93fd0..ae3afd73d2 100755 --- a/cli/test.sh +++ b/apitest/scripts/mainnet-test.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bats # -# Integration tests for bisq-cli running against a live bisq-daemon +# Smoke tests for bisq-cli running against a live bisq-daemon (on mainnet) # # Prerequisites: # -# - bats v0.4.0 must be installed (brew install bats on macOS) -# see https://github.com/sstephenson/bats/tree/v0.4.0 +# - bats-core 1.2.0+ must be installed (brew install bats-core on macOS) +# see https://github.com/bats-core/bats-core # # - Run `./bisq-daemon --apiPassword=xyz --appDataDir=$TESTDIR` where $TESTDIR # is empty or otherwise contains an unencrypted wallet with a 0 BTC balance @@ -48,14 +48,14 @@ run ./bisq-cli --password="xyz" getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.5" ] + [ "$output" = "1.3.7" ] } @test "test getversion" { run ./bisq-cli --password=xyz getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.5" ] + [ "$output" = "1.3.7" ] } @test "test setwalletpassword \"a b c\"" { @@ -190,8 +190,8 @@ [ "$output" = "Error: incorrect parameter count, expecting direction (buy|sell), currency code" ] } -@test "test getoffers buy eur check return status" { - run ./bisq-cli --password=xyz getoffers buy eur +@test "test getoffers sell eur check return status" { + run ./bisq-cli --password=xyz getoffers sell eur [ "$status" -eq 0 ] } From cb6166c65f4b03a2efcdeb29c31764906971616c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 25 Aug 2020 12:01:29 -0300 Subject: [PATCH 07/52] Remove duplicated grpc stub creation logic The :apitest GrpcStubs class was removed and recreated in the :cli subproject, to be used by both :cli and :apitest. CliMain was changed to use the new GrpcStubs. --- .../test/java/bisq/apitest/ApiTestCase.java | 9 +- .../src/test/java/bisq/apitest/GrpcStubs.java | 109 ------------------ cli/src/main/java/bisq/cli/CliMain.java | 20 +--- cli/src/main/java/bisq/cli/GrpcStubs.java | 54 +++++++++ 4 files changed, 64 insertions(+), 128 deletions(-) delete mode 100644 apitest/src/test/java/bisq/apitest/GrpcStubs.java create mode 100644 cli/src/main/java/bisq/cli/GrpcStubs.java diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index b962a2f3aa..f9100bee96 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -28,6 +28,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import bisq.apitest.config.ApiTestConfig; import bisq.apitest.method.BitcoinCliHelper; +import bisq.cli.GrpcStubs; /** * Base class for all test types: 'method', 'scenario' and 'e2e'. @@ -65,19 +66,19 @@ public class ApiTestCase { public static void setUpScaffold(String supportingApps) throws InterruptedException, ExecutionException, IOException { - // The supportingApps argument is a comma delimited string of supporting app - // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" scaffold = new Scaffold(supportingApps).setUp(); config = scaffold.config; bitcoinCli = new BitcoinCliHelper((config)); - grpcStubs = new GrpcStubs(alicedaemon, config).init(); + // For now, all grpc requests are sent to the alicedaemon, but this will need to + // be made configurable for new test cases that call arb or bob node daemons. + grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } public static void setUpScaffold(String[] params) throws InterruptedException, ExecutionException, IOException { scaffold = new Scaffold(params).setUp(); config = scaffold.config; - grpcStubs = new GrpcStubs(alicedaemon, config).init(); + grpcStubs = new GrpcStubs("localhost", alicedaemon.apiPort, config.apiPassword); } public static void tearDownScaffold() { diff --git a/apitest/src/test/java/bisq/apitest/GrpcStubs.java b/apitest/src/test/java/bisq/apitest/GrpcStubs.java deleted file mode 100644 index 6279c61489..0000000000 --- a/apitest/src/test/java/bisq/apitest/GrpcStubs.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest; - -import bisq.proto.grpc.GetVersionGrpc; -import bisq.proto.grpc.OffersGrpc; -import bisq.proto.grpc.PaymentAccountsGrpc; -import bisq.proto.grpc.WalletsGrpc; - -import io.grpc.CallCredentials; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; - -import java.util.concurrent.Executor; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static io.grpc.Status.UNAUTHENTICATED; -import static java.lang.String.format; -import static java.util.concurrent.TimeUnit.SECONDS; - - - -import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.config.BisqAppConfig; - -public class GrpcStubs { - - public final CallCredentials credentials; - public final String host; - public final int port; - - public GetVersionGrpc.GetVersionBlockingStub versionService; - public OffersGrpc.OffersBlockingStub offersService; - public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; - public WalletsGrpc.WalletsBlockingStub walletsService; - - public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { - this.credentials = new PasswordCallCredentials(config.apiPassword); - this.host = "localhost"; - this.port = bisqAppConfig.apiPort; - } - - public GrpcStubs init() { - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - channel.shutdown().awaitTermination(1, SECONDS); - } catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - })); - - this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - - return this; - } - - static class PasswordCallCredentials extends CallCredentials { - - public static final String PASSWORD_KEY = "password"; - private final String passwordValue; - - public PasswordCallCredentials(String passwordValue) { - if (passwordValue == null) - throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); - this.passwordValue = passwordValue; - } - - @Override - public void applyRequestMetadata(RequestInfo requestInfo, - Executor appExecutor, - MetadataApplier metadataApplier) { - appExecutor.execute(() -> { - try { - var headers = new Metadata(); - var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); - headers.put(passwordKey, passwordValue); - metadataApplier.apply(headers); - } catch (Throwable ex) { - metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); - } - }); - } - - @Override - public void thisUsesUnstableApi() { - // An experimental api. A noop but never called; tries to make it clearer to - // implementors that they may break in the future. - } - } -} diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 37dbe3f52d..46dfee1072 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -133,21 +133,11 @@ public class CliMain { if (password == null) throw new IllegalArgumentException("missing required 'password' option"); - var credentials = new PasswordCallCredentials(password); - - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - })); - - var versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - var walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + GrpcStubs grpcStubs = new GrpcStubs(host, port, password); + var versionService = grpcStubs.versionService; + var offersService = grpcStubs.offersService; + var paymentAccountsService = grpcStubs.paymentAccountsService; + var walletsService = grpcStubs.walletsService; try { switch (method) { diff --git a/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java new file mode 100644 index 0000000000..e12a6efa7c --- /dev/null +++ b/cli/src/main/java/bisq/cli/GrpcStubs.java @@ -0,0 +1,54 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.cli; + +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.PaymentAccountsGrpc; +import bisq.proto.grpc.WalletsGrpc; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannelBuilder; + +import static java.util.concurrent.TimeUnit.SECONDS; + +public class GrpcStubs { + + public final GetVersionGrpc.GetVersionBlockingStub versionService; + public final OffersGrpc.OffersBlockingStub offersService; + public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; + public final WalletsGrpc.WalletsBlockingStub walletsService; + + public GrpcStubs(String apiHost, int apiPort, String apiPassword) { + CallCredentials credentials = new PasswordCallCredentials(apiPassword); + + var channel = ManagedChannelBuilder.forAddress(apiHost, apiPort).usePlaintext().build(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + })); + + this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + } +} From b9c1feba9a6e49f9501fcdb4e8eb82cb146ff3f1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 25 Aug 2020 12:35:57 -0300 Subject: [PATCH 08/52] Remove redundant ApiTestConfig options This change removes three options: runArbNodeAsDesktop, runAliceNodeAsDesktop, and runBobNodeAsDesktop, which should have been deleted when the supportingApps option was added. The comma delimited list of apps passed with the supportingApps option now determines whether arbitration / bob / alice nodes are started as desktops or daemons. --- .../src/main/java/bisq/apitest/Scaffold.java | 26 +++++++++------- .../bisq/apitest/config/ApiTestConfig.java | 30 ------------------- 2 files changed, 15 insertions(+), 41 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index bf0e4c771d..d195b78701 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -311,17 +311,25 @@ public class Scaffold { bitcoinDaemon.verifyBitcoindRunning(); } + // Start Bisq apps defined by the supportingApps option, in the in proper order. + if (config.hasSupportingApp(seednode.name())) startBisqApp(seednode, executor, countdownLatch); - if (config.hasSupportingApp(arbdaemon.name(), arbdesktop.name())) - startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); + if (config.hasSupportingApp(arbdaemon.name())) + startBisqApp(arbdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(arbdesktop.name())) + startBisqApp(arbdesktop, executor, countdownLatch); - if (config.hasSupportingApp(alicedaemon.name(), alicedesktop.name())) - startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); + if (config.hasSupportingApp(alicedaemon.name())) + startBisqApp(alicedaemon, executor, countdownLatch); + else if (config.hasSupportingApp(alicedesktop.name())) + startBisqApp(alicedesktop, executor, countdownLatch); - if (config.hasSupportingApp(bobdaemon.name(), bobdesktop.name())) - startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); + if (config.hasSupportingApp(bobdaemon.name())) + startBisqApp(bobdaemon, executor, countdownLatch); + else if (config.hasSupportingApp(bobdesktop.name())) + startBisqApp(bobdesktop, executor, countdownLatch); } private void startBisqApp(BisqAppConfig bisqAppConfig, @@ -329,28 +337,24 @@ public class Scaffold { CountDownLatch countdownLatch) throws IOException, InterruptedException { - BisqApp bisqApp; + BisqApp bisqApp = createBisqApp(bisqAppConfig); switch (bisqAppConfig) { case seednode: - bisqApp = createBisqApp(seednode); seedNodeTask = new SetupTask(bisqApp, countdownLatch); seedNodeTaskFuture = executor.submit(seedNodeTask); break; case arbdaemon: case arbdesktop: - bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon); arbNodeTask = new SetupTask(bisqApp, countdownLatch); arbNodeTaskFuture = executor.submit(arbNodeTask); break; case alicedaemon: case alicedesktop: - bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon); aliceNodeTask = new SetupTask(bisqApp, countdownLatch); aliceNodeTaskFuture = executor.submit(aliceNodeTask); break; case bobdaemon: case bobdesktop: - bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon); bobNodeTask = new SetupTask(bisqApp, countdownLatch); bobNodeTaskFuture = executor.submit(bobNodeTask); break; diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 6a9e9a6448..d3c491c742 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -65,9 +65,6 @@ public class ApiTestConfig { static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; static final String API_PASSWORD = "apiPassword"; static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; - static final String RUN_ARB_NODE_AS_DESKTOP = "runArbNodeAsDesktop"; - static final String RUN_ALICE_NODE_AS_DESKTOP = "runAliceNodeAsDesktop"; - static final String RUN_BOB_NODE_AS_DESKTOP = "runBobNodeAsDesktop"; static final String BISQ_APP_INIT_TIME = "bisqAppInitTime"; static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; @@ -98,9 +95,6 @@ public class ApiTestConfig { // Daemon instances can use same gRPC password, but each needs a different apiPort. public final String apiPassword; public final boolean runSubprojectJars; - public final boolean runArbNodeAsDesktop; - public final boolean runAliceNodeAsDesktop; - public final boolean runBobNodeAsDesktop; public final long bisqAppInitTime; public final boolean skipTests; public final boolean shutdownAfterTests; @@ -202,27 +196,6 @@ public class ApiTestConfig { .ofType(Boolean.class) .defaultsTo(false); - ArgumentAcceptingOptionSpec runArbNodeAsDesktopOpt = - parser.accepts(RUN_ARB_NODE_AS_DESKTOP, - "Run Arbitration node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); // TODO how do I register mediator? - - ArgumentAcceptingOptionSpec runAliceNodeAsDesktopOpt = - parser.accepts(RUN_ALICE_NODE_AS_DESKTOP, - "Run Alice node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); - - ArgumentAcceptingOptionSpec runBobNodeAsDesktopOpt = - parser.accepts(RUN_BOB_NODE_AS_DESKTOP, - "Run Bob node as desktop") - .withRequiredArg() - .ofType(Boolean.class) - .defaultsTo(false); - ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = parser.accepts(BISQ_APP_INIT_TIME, "Amount of time (ms) to wait on a Bisq instance's initialization") @@ -302,9 +275,6 @@ public class ApiTestConfig { this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); this.apiPassword = options.valueOf(apiPasswordOpt); this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); - this.runArbNodeAsDesktop = options.valueOf(runArbNodeAsDesktopOpt); - this.runAliceNodeAsDesktop = options.valueOf(runAliceNodeAsDesktopOpt); - this.runBobNodeAsDesktop = options.valueOf(runBobNodeAsDesktopOpt); this.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt); this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); From 4d12b8f3d3dfb985eaf1abc53f3850cb75c53719 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 25 Aug 2020 12:59:55 -0300 Subject: [PATCH 09/52] Allow remote debugging of background Bisq apps A unique hard coded debug listening port is assigned to the different Bisq app types in the BisqAppConfig enum, and background Bisq apps will be started with remote debug options if the scaffold-setup method is passed an --enableBisqDebugging=true option. * Added enableBisqDebugging (default=false) option to ApiTestConfig. * Added remoteDebugPort field to BisqAppConfig enum. * Added debugOpts field to BisqApp (using BisqAppConfig#remoteDebugPort). * Appends debugOpts to exported JAVA_OPTS environment variable if present. * Removed messy quotes from BisqAppConfig enum javaOpts values. * Removed redundant return statement from BisqApp#shutdown(). --- .../bisq/apitest/config/ApiTestConfig.java | 9 ++++ .../bisq/apitest/config/BisqAppConfig.java | 42 ++++++++++++------- .../main/java/bisq/apitest/linux/BisqApp.java | 7 +++- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index d3c491c742..5197a35634 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -69,6 +69,7 @@ public class ApiTestConfig { static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; static final String SUPPORTING_APPS = "supportingApps"; + static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging"; // Default values for certain options static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; @@ -99,6 +100,7 @@ public class ApiTestConfig { public final boolean skipTests; public final boolean shutdownAfterTests; public final List supportingApps; + public final boolean enableBisqDebugging; // Immutable system configurations set in the constructor. public final String bitcoinDatadir; @@ -224,6 +226,12 @@ public class ApiTestConfig { .ofType(String.class) .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); + ArgumentAcceptingOptionSpec enableBisqDebuggingOpt = + parser.accepts(ENABLE_BISQ_DEBUGGING, + "Start Bisq apps with remote debug options") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); try { CompositeOptionSet options = new CompositeOptionSet(); @@ -279,6 +287,7 @@ public class ApiTestConfig { this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); + this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt); // Assign values to special-case static fields. BASH_PATH_VALUE = bashPath; diff --git a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java index 662956462e..08a7531ca3 100644 --- a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java @@ -30,58 +30,64 @@ import bisq.daemon.app.BisqDaemonMain; @see dev-setup.md @see dao-setup.md */ -@SuppressWarnings("unused") public enum BisqAppConfig { seednode("bisq-BTC_REGTEST_Seed_2002", "bisq-seednode", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", SeedNodeMain.class.getName(), 2002, 5120, - -1), + -1, + 49996), arbdaemon("bisq-BTC_REGTEST_Arb_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 4444, 5121, - 9997), + 9997, + 49997), arbdesktop("bisq-BTC_REGTEST_Arb_dao", "bisq-desktop", - "\"-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 4444, 5121, - -1), + -1, + 49997), alicedaemon("bisq-BTC_REGTEST_Alice_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 7777, 5122, - 9998), + 9998, + 49998), alicedesktop("bisq-BTC_REGTEST_Alice_dao", "bisq-desktop", - "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 7777, 5122, - -1), + -1, + 49998), bobdaemon("bisq-BTC_REGTEST_Bob_dao", "bisq-daemon", - "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqDaemonMain.class.getName(), 8888, 5123, - 9999), + 9999, + 49999), bobdesktop("bisq-BTC_REGTEST_Bob_dao", "bisq-desktop", - "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + "-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml", BisqAppMain.class.getName(), 8888, 5123, - -1); + -1, + 49999); public final String appName; public final String startupScript; @@ -91,6 +97,7 @@ public enum BisqAppConfig { public final int rpcBlockNotificationPort; // Daemons can use a global gRPC password, but each needs a unique apiPort. public final int apiPort; + public final int remoteDebugPort; BisqAppConfig(String appName, String startupScript, @@ -98,7 +105,8 @@ public enum BisqAppConfig { String mainClassName, int nodePort, int rpcBlockNotificationPort, - int apiPort) { + int apiPort, + int remoteDebugPort) { this.appName = appName; this.startupScript = startupScript; this.javaOpts = javaOpts; @@ -106,6 +114,7 @@ public enum BisqAppConfig { this.nodePort = nodePort; this.rpcBlockNotificationPort = rpcBlockNotificationPort; this.apiPort = apiPort; + this.remoteDebugPort = remoteDebugPort; } @Override @@ -118,6 +127,7 @@ public enum BisqAppConfig { ", nodePort=" + nodePort + "\n" + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + ", apiPort=" + apiPort + "\n" + + ", remoteDebugPort=" + remoteDebugPort + "\n" + '}'; } } diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index f449d0b98f..7c66746f3d 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -53,6 +53,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { private final boolean useLocalhostForP2P; public final boolean useDevPrivilegeKeys; private final String findBisqPidScript; + private final String debugOpts; public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { super(bisqAppConfig.appName, config); @@ -67,6 +68,9 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { this.useDevPrivilegeKeys = true; this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest") + "/scripts/get-bisq-pid.sh"; + this.debugOpts = config.enableBisqDebugging + ? " -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:" + bisqAppConfig.remoteDebugPort + : ""; } @Override @@ -112,7 +116,6 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { if (isAlive(pid)) { this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName))); - return; } } catch (Exception e) { @@ -209,7 +212,7 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { } private String getJavaOptsSpec() { - return "export JAVA_OPTS=" + bisqAppConfig.javaOpts + "; "; + return "export JAVA_OPTS=\"" + bisqAppConfig.javaOpts + debugOpts + "\"; "; } private List getOptsList() { From 5433707fa254bc7aff8797a5660d923389bf4bf1 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 21:30:33 -0500 Subject: [PATCH 10/52] Bundle broadcast requests --- .../bisq/network/p2p/BundleOfEnvelopes.java | 8 +- .../java/bisq/network/p2p/P2PService.java | 56 +-- .../network/p2p/peers/BroadcastHandler.java | 351 +++++++++--------- .../bisq/network/p2p/peers/Broadcaster.java | 62 +++- 4 files changed, 261 insertions(+), 216 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java index d4ff8f46a8..c57e0d530e 100644 --- a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java +++ b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java @@ -44,6 +44,10 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended this(new ArrayList<>(), Version.getP2PMessageVersion()); } + public BundleOfEnvelopes(List envelopes) { + this(envelopes, Version.getP2PMessageVersion()); + } + public void add(NetworkEnvelope networkEnvelope) { envelopes.add(networkEnvelope); } @@ -67,7 +71,9 @@ public final class BundleOfEnvelopes extends NetworkEnvelope implements Extended .build(); } - public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, NetworkProtoResolver resolver, int messageVersion) { + public static BundleOfEnvelopes fromProto(protobuf.BundleOfEnvelopes proto, + NetworkProtoResolver resolver, + int messageVersion) { List envelopes = proto.getEnvelopesList() .stream() .map(envelope -> { diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 5caab46ec4..563c3905b7 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -37,7 +37,6 @@ import bisq.network.p2p.seed.SeedNodeRepository; import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.messages.AddDataMessage; -import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.network.p2p.storage.messages.RefreshOfferMessage; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.network.p2p.storage.payload.MailboxStoragePayload; @@ -53,7 +52,6 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtobufferException; import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.persistable.PersistedDataHost; -import bisq.common.util.Utilities; import com.google.inject.Inject; @@ -77,6 +75,7 @@ import java.security.PublicKey; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -446,7 +445,8 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } @Override - public void onRemoved(Collection protectedStorageEntries) { } + public void onRemoved(Collection protectedStorageEntries) { + } /////////////////////////////////////////////////////////////////////////////////////////// // DirectMessages @@ -463,7 +463,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } } - private void doSendEncryptedDirectMessage(@NotNull NodeAddress peersNodeAddress, PubKeyRing pubKeyRing, NetworkEnvelope message, + private void doSendEncryptedDirectMessage(@NotNull NodeAddress peersNodeAddress, + PubKeyRing pubKeyRing, + NetworkEnvelope message, SendDirectMessageListener sendDirectMessageListener) { log.debug("Send encrypted direct message {} to peer {}", message.getClass().getSimpleName(), peersNodeAddress); @@ -667,41 +669,21 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override - public void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts) { + public void onSufficientlyBroadcast(List broadcastRequests) { + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getMessage() instanceof AddDataMessage) + .filter(broadcastRequest -> { + AddDataMessage addDataMessage = (AddDataMessage) broadcastRequest.getMessage(); + return addDataMessage.getProtectedStorageEntry().equals(protectedMailboxStorageEntry); + }) + .forEach(e -> sendMailboxMessageListener.onStoredInMailbox()); } @Override - public void onBroadcastedToFirstPeer(BroadcastMessage message) { - // The reason for that check was to separate different callback for different send calls. - // We only want to notify our sendMailboxMessageListener for the calls he is interested in. - if (message instanceof AddDataMessage && - ((AddDataMessage) message).getProtectedStorageEntry().equals(protectedMailboxStorageEntry)) { - // We delay a bit to give more time for sufficient propagation in the P2P network. - // This should help to avoid situations where a user closes the app too early and the msg - // does not arrive. - // We could use onBroadcastCompleted instead but it might take too long if one peer - // is very badly connected. - // TODO We could check for a certain threshold of no. of incoming network_messages of the same msg - // to see how well it is propagated. BitcoinJ uses such an approach for tx propagation. - UserThread.runAfter(() -> { - log.info("Broadcasted to first peer (3 sec. ago): Message = {}", Utilities.toTruncatedString(message)); - sendMailboxMessageListener.onStoredInMailbox(); - }, 3); - } - } - - @Override - public void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts) { - log.info("Broadcast completed: Sent to {} peers (failed: {}). Message = {}", - numOfCompletedBroadcasts, numOfFailedBroadcasts, Utilities.toTruncatedString(message)); - if (numOfCompletedBroadcasts == 0) - sendMailboxMessageListener.onFault("Broadcast completed without any successful broadcast"); - } - - @Override - public void onBroadcastFailed(String errorMessage) { - // TODO investigate why not sending sendMailboxMessageListener.onFault. Related probably - // to the logic from BroadcastHandler.sendToPeer + public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) { + sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" + + "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" + + "numOfFailedBroadcast=" + numOfFailedBroadcast); } }; boolean result = p2PDataStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener); @@ -714,7 +696,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis log.error("Unexpected state: adding mailbox message that already exists."); } } catch (CryptoException e) { - log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); + log.error("Signing at getMailboxDataWithSignedSeqNr failed."); } } else { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index facdba5eef..530cdbaaa6 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -17,14 +17,13 @@ package bisq.network.p2p.peers; +import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.common.Timer; import bisq.common.UserThread; -import bisq.common.util.Utilities; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -33,7 +32,6 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -41,12 +39,10 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; @Slf4j public class BroadcastHandler implements PeerManager.Listener { - // TODO check if that is not too low - private static final long BASE_TIMEOUT_MS = 60; + private static final long BASE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(60); /////////////////////////////////////////////////////////////////////////////////////////// @@ -55,18 +51,12 @@ public class BroadcastHandler implements PeerManager.Listener { interface ResultHandler { void onCompleted(BroadcastHandler broadcastHandler); - - void onFault(BroadcastHandler broadcastHandler); } public interface Listener { - void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts); + void onSufficientlyBroadcast(List broadcastRequests); - void onBroadcastedToFirstPeer(BroadcastMessage message); - - void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts); - - void onBroadcastFailed(String errorMessage); + void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast); } @@ -75,16 +65,12 @@ public class BroadcastHandler implements PeerManager.Listener { /////////////////////////////////////////////////////////////////////////////////////////// private final NetworkNode networkNode; - private final String uid; private final PeerManager peerManager; - private boolean stopped = false; - private int numOfCompletedBroadcasts = 0; - private int numOfFailedBroadcasts = 0; - private BroadcastMessage message; - private ResultHandler resultHandler; - @Nullable - private Listener listener; - private int numPeers; + private final ResultHandler resultHandler; + private final String uid; + + private boolean stopped, timeoutTriggered; + private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeers; private Timer timeoutTimer; @@ -92,156 +78,80 @@ public class BroadcastHandler implements PeerManager.Listener { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - BroadcastHandler(NetworkNode networkNode, PeerManager peerManager) { + BroadcastHandler(NetworkNode networkNode, PeerManager peerManager, ResultHandler resultHandler) { this.networkNode = networkNode; this.peerManager = peerManager; + this.resultHandler = resultHandler; uid = UUID.randomUUID().toString(); peerManager.addListener(this); } - public void cancel() { - stopped = true; - onFault("Broadcast canceled.", false); - } - /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(BroadcastMessage message, - @Nullable NodeAddress sender, - ResultHandler resultHandler, - @Nullable Listener listener) { - this.message = message; - this.resultHandler = resultHandler; - this.listener = listener; + public void broadcast(List broadcastRequests) { + List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); + Collections.shuffle(confirmedConnections); - Set connectedPeersSet = networkNode.getConfirmedConnections() - .stream() - .filter(connection -> connection.getPeersNodeAddressOptional().isPresent() && - !connection.getPeersNodeAddressOptional().get().equals(sender)) // We don't broadcast back to sender - .collect(Collectors.toSet()); - - if (connectedPeersSet.isEmpty()) { - onFault("Message not broadcasted because we have no available peers yet.\n\t" + - "message = " + Utilities.toTruncatedString(message), false); - return; - } - - numOfCompletedBroadcasts = 0; - - List connectedPeersList = new ArrayList<>(connectedPeersSet); - Collections.shuffle(connectedPeersList); int delay; - - // If we are the owner of the data we broadcast faster and with higher resilience - boolean isDataOwner = sender != null && sender.equals(networkNode.getNodeAddress()); - if (isDataOwner) { - numPeers = connectedPeersList.size(); + if (requestsContainOwnMessage(broadcastRequests)) { + // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and + // with shorter delay + numPeers = confirmedConnections.size(); delay = 50; } else { - // Relay nodes do not broadcast to all connected nodes - numPeers = Math.min(7, connectedPeersList.size()); + // Relay nodes only send to max 7 peers and with longer delay + numPeers = Math.min(7, confirmedConnections.size()); delay = 100; } - long timeoutDelay = BASE_TIMEOUT_MS + delay * numPeers; - timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions - String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec."; - - log.warn(errorMessage + "\n\t" + - "numOfPeers=" + numPeers + "\n\t" + - "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" + - "numOfFailedBroadcasts=" + numOfFailedBroadcasts); - onFault(errorMessage, true); - }, timeoutDelay); - - log.debug("Broadcast message to {} peers out of {} total connected peers.", numPeers, connectedPeersSet.size()); - for (int i = 0; i < numPeers; i++) { - if (stopped) - break; // Do not continue sending after a timeout or a cancellation - + setupTimeoutHandler(broadcastRequests, delay); // todo missing 1 delay if we start with 1 + int iterations = numPeers; + for (int i = 0; i < iterations; i++) { long minDelay = (i + 1) * delay; long maxDelay = (i + 2) * delay; - Connection connection = connectedPeersList.get(i); - UserThread.runAfterRandomDelay(() -> sendToPeer(connection, message), minDelay, maxDelay, TimeUnit.MILLISECONDS); + Connection connection = confirmedConnections.get(i); + UserThread.runAfterRandomDelay(() -> { + if (stopped) { + return; + } + + // We use broadcastRequests which have excluded the requests for messages the connection has + // originated to avoid sending back the message we received. We also remove messages not satisfying + // capability checks. + List broadcastRequestsForConnection = getBroadcastRequestsForConnection(connection, broadcastRequests); + + // Could be empty list... + if (broadcastRequestsForConnection.isEmpty()) { + // We decrease numPeers in that case for making completion checks correct. + if (numPeers > 0) { + numPeers--; + } + checkForCompletion(); + return; + } + + if (connection.isStopped()) { + // Connection has died in the meantime. We skip it. + // We decrease numPeers in that case for making completion checks correct. + if (numPeers > 0) { + numPeers--; + } + checkForCompletion(); + return; + } + + sendToPeer(connection, broadcastRequestsForConnection); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } - private void sendToPeer(Connection connection, BroadcastMessage message) { - String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" + - "message = " + Utilities.toTruncatedString(message); - - if (stopped) { - onFault("Handler stopped already: " + errorMessage, false); - return; - } - - if (connection.isStopped()) { - onFault("Connection stopped already.", false); - return; - } - - if (!connection.noCapabilityRequiredOrCapabilityIsSupported(message)) { - onFault("Peer does not support requires capability.", false); - return; - } - - if (!connection.getPeersNodeAddressOptional().isPresent()) { - onFault("Peer node address is not present.", false); - return; - } - - NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get(); - SettableFuture future = networkNode.sendMessage(connection, message); - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(Connection connection) { - numOfCompletedBroadcasts++; - - if (stopped) { - // TODO investigate why that is called very often at seed nodes - onFault("stopped at onSuccess: " + errorMessage, false); - return; - } - - if (listener != null) { - listener.onBroadcasted(message, numOfCompletedBroadcasts); - - if (numOfCompletedBroadcasts == 1) { - listener.onBroadcastedToFirstPeer(message); - } - } - - - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { - if (listener != null) { - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); - } - - cleanup(); - resultHandler.onCompleted(BroadcastHandler.this); - } - } - - @Override - public void onFailure(@NotNull Throwable throwable) { - numOfFailedBroadcasts++; - - if (stopped) { - onFault("stopped at onFailure: " + errorMessage, true); - return; - } - - log.warn("Broadcast to {} failed. ErrorMessage={}", nodeAddress, throwable.getMessage()); - - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { - onFault("Last broadcast cause a failure: " + errorMessage, true); - } - } - }); + public void cancel() { + stopped = true; + cleanup(); } @@ -251,7 +161,7 @@ public class BroadcastHandler implements PeerManager.Listener { @Override public void onAllConnectionsLost() { - onFault("All connections lost", false); + cleanup(); } @Override @@ -267,34 +177,133 @@ public class BroadcastHandler implements PeerManager.Listener { // Private /////////////////////////////////////////////////////////////////////////////////////////// + // Check if we have at least one message originated by ourselves + private boolean requestsContainOwnMessage(List broadcastRequests) { + NodeAddress myAddress = networkNode.getNodeAddress(); + if (myAddress == null) + return false; + + return broadcastRequests.stream().anyMatch(e -> myAddress.equals(e.getSender())); + } + + private void setupTimeoutHandler(List broadcastRequests, int delay) { + long timeoutDelay = BASE_TIMEOUT_MS + delay * (numPeers + 1); // We added 1 in the loop + timeoutTimer = UserThread.runAfter(() -> { + if (stopped) { + return; + } + + timeoutTriggered = true; + + String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec." + "\n" + + "numOfPeers=" + numPeers + "\n" + + "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n" + + "numOfFailedBroadcasts=" + numOfFailedBroadcasts; + + log.warn(errorMessage); + + maybeNotifyListeners(broadcastRequests); + + cleanup(); + + }, timeoutDelay); + } + + // We exclude the requests containing a message we received from that connection + // Also we filter out messages which requires a capability but peer does not support it. + private List getBroadcastRequestsForConnection(Connection connection, + List broadcastRequests) { + return broadcastRequests.stream() + .filter(broadcastRequest -> !connection.getPeersNodeAddressOptional().isPresent() || + !connection.getPeersNodeAddressOptional().get().equals(broadcastRequest.getSender())) + .filter(broadcastRequest -> connection.noCapabilityRequiredOrCapabilityIsSupported(broadcastRequest.getMessage())) + .collect(Collectors.toList()); + } + + private BundleOfEnvelopes getBundle(List broadcastRequests) { + return new BundleOfEnvelopes(broadcastRequests.stream() + .map(Broadcaster.BroadcastRequest::getMessage) + .collect(Collectors.toList())); + } + + private void sendToPeer(Connection connection, List broadcastRequestsForConnection) { + BundleOfEnvelopes bundleForConnection = getBundle(broadcastRequestsForConnection); + SettableFuture future = networkNode.sendMessage(connection, bundleForConnection); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + numOfCompletedBroadcasts++; + + if (stopped) { + return; + } + + maybeNotifyListeners(broadcastRequestsForConnection); + + checkForCompletion(); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.warn("Broadcast to {} failed. ErrorMessage={}", connection.getPeersNodeAddressOptional(), + throwable.getMessage()); + + numOfFailedBroadcasts++; + + if (stopped) { + return; + } + + maybeNotifyListeners(broadcastRequestsForConnection); + + checkForCompletion(); + } + }); + } + + private void maybeNotifyListeners(List broadcastRequests) { + // We use equal checks to avoid duplicated listener calls as it would be the case with >= checks. + if (numOfCompletedBroadcasts == 3) { + // We have heard back from 3 peers so we consider the message was sufficiently broadcast. + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getListener() != null) + .map(Broadcaster.BroadcastRequest::getListener) + .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); + } else { + int maxPossibleSuccessCases = numPeers - numOfFailedBroadcasts; + if (maxPossibleSuccessCases == 2) { + // We never can reach required resilience as too many numOfFailedBroadcasts occurred. + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getListener() != null) + .map(Broadcaster.BroadcastRequest::getListener) + .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); + } else if (timeoutTriggered && numOfCompletedBroadcasts < 3) { + // We did not reach resilience level and timeout prevents to reach it later + broadcastRequests.stream() + .filter(broadcastRequest -> broadcastRequest.getListener() != null) + .map(Broadcaster.BroadcastRequest::getListener) + .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); + } + } + } + + private void checkForCompletion() { + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { + cleanup(); + } + } + private void cleanup() { stopped = true; - peerManager.removeListener(this); if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } + peerManager.removeListener(this); + resultHandler.onCompleted(this); } - private void onFault(String errorMessage, boolean logWarning) { - cleanup(); - - if (logWarning) { - log.warn(errorMessage); - } - - if (listener != null) { - listener.onBroadcastFailed(errorMessage); - - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers || stopped) { - listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts); - } - } - - resultHandler.onFault(this); - } - - @Override public boolean equals(Object o) { if (this == o) return true; @@ -302,7 +311,7 @@ public class BroadcastHandler implements PeerManager.Listener { BroadcastHandler that = (BroadcastHandler) o; - return !(uid != null ? !uid.equals(that.uid) : that.uid != null); + return uid.equals(that.uid); } @Override diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 919d64543b..6640c5f990 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -21,17 +21,31 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.common.Timer; +import bisq.common.UserThread; + import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; +@Slf4j public class Broadcaster implements BroadcastHandler.ResultHandler { + private static final long BROADCAST_INTERVAL_MS = 1000; + private final NetworkNode networkNode; private final PeerManager peerManager; private final Set broadcastHandlers = new CopyOnWriteArraySet<>(); + private final List broadcastRequests = new ArrayList<>(); + private Timer timer; /////////////////////////////////////////////////////////////////////////////////////////// @@ -46,7 +60,10 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { public void shutDown() { broadcastHandlers.forEach(BroadcastHandler::cancel); - broadcastHandlers.clear(); + + if (timer != null) { + timer.stop(); + } } @@ -59,12 +76,26 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { broadcast(message, sender, null); } + public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { - BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager); - broadcastHandler.broadcast(message, sender, this, listener); - broadcastHandlers.add(broadcastHandler); + broadcastRequests.add(new BroadcastRequest(message, sender, listener)); + + if (timer == null) { + timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + } + + private void maybeBroadcastBundle() { + if (!broadcastRequests.isEmpty()) { + BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests)); + broadcastHandlers.add(broadcastHandler); + broadcastRequests.clear(); + + timer = null; + } } @@ -77,8 +108,25 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { broadcastHandlers.remove(broadcastHandler); } - @Override - public void onFault(BroadcastHandler broadcastHandler) { - broadcastHandlers.remove(broadcastHandler); + + /////////////////////////////////////////////////////////////////////////////////////////// + // BroadcastRequest class + /////////////////////////////////////////////////////////////////////////////////////////// + + @Value + public static class BroadcastRequest { + private BroadcastMessage message; + @Nullable + private NodeAddress sender; + @Nullable + private BroadcastHandler.Listener listener; + + private BroadcastRequest(BroadcastMessage message, + @Nullable NodeAddress sender, + @Nullable BroadcastHandler.Listener listener) { + this.message = message; + this.sender = sender; + this.listener = listener; + } } } From 06f407fda3a1e6e4808df521a4ae8e339d778903 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 21:31:06 -0500 Subject: [PATCH 11/52] Add todo and curly brackets --- .../main/java/bisq/network/p2p/network/Connection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 3f315b77cb..050978d969 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -225,7 +225,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { // Called from various threads public void sendMessage(NetworkEnvelope networkEnvelope) { - log.debug(">> Send networkEnvelope of type: " + networkEnvelope.getClass().getSimpleName()); + log.debug(">> Send networkEnvelope of type: {}", networkEnvelope.getClass().getSimpleName()); if (!stopped) { if (noCapabilityRequiredOrCapabilityIsSupported(networkEnvelope)) { @@ -319,6 +319,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { } } + // TODO: If msg is BundleOfEnvelopes we should check each individual message for capability and filter out those + // which fail. public boolean noCapabilityRequiredOrCapabilityIsSupported(Proto msg) { boolean result; if (msg instanceof AddDataMessage) { @@ -408,12 +410,13 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { checkArgument(connection.equals(this)); - if (networkEnvelope instanceof BundleOfEnvelopes) + if (networkEnvelope instanceof BundleOfEnvelopes) { for (NetworkEnvelope current : ((BundleOfEnvelopes) networkEnvelope).getEnvelopes()) { UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(current, connection))); } - else + } else { UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection))); + } } From b8152d68aeaf417783c8da6f8678435c2dc352b3 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 22:42:48 -0500 Subject: [PATCH 12/52] Change BROADCAST_INTERVAL_MS to 2 sec. --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 6640c5f990..5e2851a842 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -39,7 +39,7 @@ import org.jetbrains.annotations.Nullable; @Slf4j public class Broadcaster implements BroadcastHandler.ResultHandler { - private static final long BROADCAST_INTERVAL_MS = 1000; + private static final long BROADCAST_INTERVAL_MS = 2000; private final NetworkNode networkNode; private final PeerManager peerManager; From 04b6c2abea0c738a96d58a35cd10a84fdc445b0e Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 22:43:24 -0500 Subject: [PATCH 13/52] Don't wrap into BundleOfEnvelopes is only 1 message is used --- .../network/p2p/peers/BroadcastHandler.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 530cdbaaa6..d15a869e38 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -24,6 +24,7 @@ import bisq.network.p2p.network.NetworkNode; import bisq.common.Timer; import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -220,15 +221,9 @@ public class BroadcastHandler implements PeerManager.Listener { .collect(Collectors.toList()); } - private BundleOfEnvelopes getBundle(List broadcastRequests) { - return new BundleOfEnvelopes(broadcastRequests.stream() - .map(Broadcaster.BroadcastRequest::getMessage) - .collect(Collectors.toList())); - } - private void sendToPeer(Connection connection, List broadcastRequestsForConnection) { - BundleOfEnvelopes bundleForConnection = getBundle(broadcastRequestsForConnection); - SettableFuture future = networkNode.sendMessage(connection, bundleForConnection); + NetworkEnvelope networkEnvelope = getNetworkEnvelope(broadcastRequestsForConnection); + SettableFuture future = networkNode.sendMessage(connection, networkEnvelope); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -262,6 +257,17 @@ public class BroadcastHandler implements PeerManager.Listener { }); } + private NetworkEnvelope getNetworkEnvelope(List broadcastRequests) { + if (broadcastRequests.size() == 1) { + // If we only have 1 message we avoid the overhead of the BundleOfEnvelopes and send the message directly + return broadcastRequests.get(0).getMessage(); + } else { + return new BundleOfEnvelopes(broadcastRequests.stream() + .map(Broadcaster.BroadcastRequest::getMessage) + .collect(Collectors.toList())); + } + } + private void maybeNotifyListeners(List broadcastRequests) { // We use equal checks to avoid duplicated listener calls as it would be the case with >= checks. if (numOfCompletedBroadcasts == 3) { From f67a46791b03b138a15af939dc0d261bfbbc71e9 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 22:49:12 -0500 Subject: [PATCH 14/52] LAST_ACTIVITY_AGE_MS should be millisec not sec --- .../java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java index 6115df52e2..6d8ad42540 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java @@ -50,7 +50,7 @@ public class KeepAliveManager implements MessageListener, ConnectionListener, Pe private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class); private static final int INTERVAL_SEC = new Random().nextInt(5) + 30; - private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC / 2; + private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC * 1000 / 2; private final NetworkNode networkNode; private final PeerManager peerManager; From c6c56b35f9aca10bc327c870645bdc6c225452eb Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 22:50:36 -0500 Subject: [PATCH 15/52] Increase INTERVAL_SEC to 30-60 sec from 5-35 sec. --- .../java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java index 6d8ad42540..6ace6d87e9 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/keepalive/KeepAliveManager.java @@ -49,7 +49,7 @@ import org.jetbrains.annotations.NotNull; public class KeepAliveManager implements MessageListener, ConnectionListener, PeerManager.Listener { private static final Logger log = LoggerFactory.getLogger(KeepAliveManager.class); - private static final int INTERVAL_SEC = new Random().nextInt(5) + 30; + private static final int INTERVAL_SEC = new Random().nextInt(30) + 30; private static final long LAST_ACTIVITY_AGE_MS = INTERVAL_SEC * 1000 / 2; private final NetworkNode networkNode; From d59a3adbc30e9e9461d72095d6f356dbeae7bf82 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 25 Aug 2020 23:30:52 -0500 Subject: [PATCH 16/52] Add logs --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 5e2851a842..a0ef75257a 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -81,6 +81,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { broadcastRequests.add(new BroadcastRequest(message, sender, listener)); + log.info("Broadcast requested for {}. We queue it up for next bundled broadcast.", + message.getClass().getSimpleName()); if (timer == null) { timer = UserThread.runAfter(this::maybeBroadcastBundle, BROADCAST_INTERVAL_MS, TimeUnit.MILLISECONDS); @@ -89,9 +91,10 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private void maybeBroadcastBundle() { if (!broadcastRequests.isEmpty()) { + log.info("Broadcast bundled requests of {} messages", broadcastRequests.size()); BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); - broadcastHandler.broadcast(new ArrayList<>(broadcastRequests)); broadcastHandlers.add(broadcastHandler); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests)); broadcastRequests.clear(); timer = null; From d8da20acee4fb258cae88df6fae295c8ff25a045 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 26 Aug 2020 00:20:47 -0500 Subject: [PATCH 17/52] Remove TODO --- p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index d15a869e38..7ffe569360 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -109,7 +109,8 @@ public class BroadcastHandler implements PeerManager.Listener { delay = 100; } - setupTimeoutHandler(broadcastRequests, delay); // todo missing 1 delay if we start with 1 + setupTimeoutHandler(broadcastRequests, delay); + int iterations = numPeers; for (int i = 0; i < iterations; i++) { long minDelay = (i + 1) * delay; From 61d730abf9208408847c48c929f4eedce0bd2119 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Tue, 25 Aug 2020 20:16:52 +0200 Subject: [PATCH 18/52] Add Tether token support via ERC20 and Omni --- .../bisq/asset/LiquidBitcoinAddressValidator.java | 12 ++++++++++++ .../main/java/bisq/asset/coins/LiquidBitcoin.java | 3 ++- .../main/java/bisq/asset/coins/TetherUSDLiquid.java | 12 ++++++++++++ .../main/java/bisq/asset/coins/TetherUSDOmni.java | 12 ++++++++++++ .../main/java/bisq/asset/tokens/TetherUSDERC20.java | 11 +++++++++++ .../resources/META-INF/services/bisq.asset.Asset | 3 +++ .../java/bisq/core/provider/price/PriceProvider.java | 11 ++++++++++- .../src/main/java/bisq/desktop/main/MainView.java | 3 +-- 8 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java create mode 100644 assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java create mode 100644 assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java create mode 100644 assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java diff --git a/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java new file mode 100644 index 0000000000..b8861bb108 --- /dev/null +++ b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java @@ -0,0 +1,12 @@ +package bisq.asset; + +public class LiquidBitcoinAddressValidator extends RegexAddressValidator { + static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$"; + public LiquidBitcoinAddressValidator() { + super(REGEX); + } + + public LiquidBitcoinAddressValidator(String regex, String errorMessageI18nKey) { + super(REGEX, "validation.altcoin.liquidBitcoin.invalidAddress"); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java index 6d77350994..3c0be367b0 100644 --- a/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java +++ b/assets/src/main/java/bisq/asset/coins/LiquidBitcoin.java @@ -19,12 +19,13 @@ package bisq.asset.coins; import bisq.asset.AltCoinAccountDisclaimer; import bisq.asset.Coin; +import bisq.asset.LiquidBitcoinAddressValidator; import bisq.asset.RegexAddressValidator; @AltCoinAccountDisclaimer("account.altcoin.popup.liquidbitcoin.msg") public class LiquidBitcoin extends Coin { public LiquidBitcoin() { - super("Liquid Bitcoin", "L-BTC", new RegexAddressValidator("^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$", "validation.altcoin.liquidBitcoin.invalidAddress")); + super("Liquid Bitcoin", "L-BTC", new LiquidBitcoinAddressValidator()); } } diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java b/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java new file mode 100644 index 0000000000..b5b50a8ad1 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java @@ -0,0 +1,12 @@ +package bisq.asset.coins; + +import bisq.asset.Coin; +import bisq.asset.LiquidBitcoinAddressValidator; + +public class TetherUSDLiquid extends Coin { + public TetherUSDLiquid() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (Liquid Bitcoin)", "L-USDT", new LiquidBitcoinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java b/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java new file mode 100644 index 0000000000..b0ab624d71 --- /dev/null +++ b/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java @@ -0,0 +1,12 @@ +package bisq.asset.coins; + +import bisq.asset.Base58BitcoinAddressValidator; +import bisq.asset.Coin; + +public class TetherUSDOmni extends Coin { + public TetherUSDOmni() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (Omni)", "USDT-O", new Base58BitcoinAddressValidator()); + } +} diff --git a/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java new file mode 100644 index 0000000000..cb57361a1f --- /dev/null +++ b/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java @@ -0,0 +1,11 @@ +package bisq.asset.tokens; + +import bisq.asset.Erc20Token; + +public class TetherUSDERC20 extends Erc20Token { + public TetherUSDERC20() { + // If you add a new USDT variant or want to change this ticker symbol you should also look here: + // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() + super("Tether USD (ERC20)", "USDT-E"); + } +} diff --git a/assets/src/main/resources/META-INF/services/bisq.asset.Asset b/assets/src/main/resources/META-INF/services/bisq.asset.Asset index 80a6168463..a2f77816d6 100644 --- a/assets/src/main/resources/META-INF/services/bisq.asset.Asset +++ b/assets/src/main/resources/META-INF/services/bisq.asset.Asset @@ -104,6 +104,8 @@ bisq.asset.coins.Spectrecoin bisq.asset.coins.Starwels bisq.asset.coins.SUB1X bisq.asset.coins.TEO +bisq.asset.coins.TetherUSDLiquid +bisq.asset.coins.TetherUSDOmni bisq.asset.coins.TurtleCoin bisq.asset.coins.UnitedCommunityCoin bisq.asset.coins.Unobtanium @@ -123,6 +125,7 @@ bisq.asset.coins.ZeroClassic bisq.asset.tokens.AugmintEuro bisq.asset.tokens.DaiStablecoin bisq.asset.tokens.EtherStone +bisq.asset.tokens.TetherUSDERC20 bisq.asset.tokens.TrueUSD bisq.asset.tokens.USDCoin bisq.asset.tokens.VectorspaceAI diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java index d0ac27c2d7..184be90435 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceProvider.java +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -70,7 +70,12 @@ public class PriceProvider extends HttpClientProvider { final double price = (Double) treeMap.get("price"); // json uses double for our timestampSec long value... final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); - marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + if (currencyCode.equals("USDT")) { + addPrice(marketPriceMap, "USDT-O", price, timestampSec); + addPrice(marketPriceMap, "USDT-E", price, timestampSec); + addPrice(marketPriceMap, "L-USDT", price, timestampSec); + } + addPrice(marketPriceMap, currencyCode, price, timestampSec); } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); @@ -80,6 +85,10 @@ public class PriceProvider extends HttpClientProvider { return new Tuple2<>(tsMap, marketPriceMap); } + private void addPrice(Map marketPriceMap, String currencyCode, double price, long timestampSec) { + marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); + } + public String getBaseUrl() { return httpClient.getBaseUrl(); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 981c819ea2..cca1f6c164 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -534,11 +534,10 @@ public class MainView extends InitializableView String selectedCurrencyCode = model.getPriceFeedService().getCurrencyCode(); MarketPrice selectedMarketPrice = model.getPriceFeedService().getMarketPrice(selectedCurrencyCode); - return Res.get("mainView.marketPrice.tooltip", "Bisq Price Index for " + selectedCurrencyCode, "", - DisplayUtils.formatTime(new Date(selectedMarketPrice.getTimestampSec())), + selectedMarketPrice != null ? DisplayUtils.formatTime(new Date(selectedMarketPrice.getTimestampSec())) : Res.get("shared.na"), model.getPriceFeedService().getProviderNodeAddress()); } From 2d3b29a8f394e109cea9e6d92e50b19e27293b5f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 27 Aug 2020 16:34:52 -0300 Subject: [PATCH 19/52] Upgrade JFoenix to v9.0.10 This commit & PR upgrades JFoenix from v9.0.6 to v9.0.10, to avoid an NPE thrown when registering a DisputeAgent in an arbitartor (regtest) desktop's account view. The JFoenix com.jfoenix.adapters.ReflectionHelper class has a getField method that silently swallows a Throwable and returns null. After clicking ALT-D or ALT-N in the an arbitrator's desktop -> accounts view (to register dispute agents) a private field cannot be accessed via reflection, and bisq.desktop.components.JFXTextFieldSkinBisqStyle#updateTextPos() throws an NPE. The cause of the NPE is due to a failure to create the textNode value in the JFXTextFieldSkinBisqStyle constructor: textNode = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textNode"); If this happens,the UI becomes unusable -- many views are blank. --- build.gradle | 2 +- gradle/witness/gradle-witness.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ab86fc152a..5c238c9701 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ configure(subprojects) { javaxAnnotationVersion = '1.2' jcsvVersion = '1.4.0' jetbrainsAnnotationsVersion = '13.0' - jfoenixVersion = '9.0.6' + jfoenixVersion = '9.0.10' joptVersion = '5.0.4' jsonsimpleVersion = '1.1.1' junitVersion = '4.12' diff --git a/gradle/witness/gradle-witness.gradle b/gradle/witness/gradle-witness.gradle index 08f74f9f51..856355e473 100644 --- a/gradle/witness/gradle-witness.gradle +++ b/gradle/witness/gradle-witness.gradle @@ -42,7 +42,7 @@ dependencyVerification { 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', - 'com.jfoenix:jfoenix:4739e37a05e67c3bc9d5b391a1b93717b5a48fa872992616b0964d3f827f8fe6', + 'com.jfoenix:jfoenix:8060235fec5eb49617ec8d81d379e8c945f6cc722d0645e97190045100de2084', 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'com.nativelibs4java:bridj:101bcd9b6637e6bc16e56deb3daefba62b1f5e8e9e37e1b3e56e3b5860d659cf', From b1702f7a6d5fa6a9dcf4bf8dfed89adb59726a1d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 15:31:41 -0500 Subject: [PATCH 20/52] - Add shutdown handling to broadCaster. It is important that we flush our queued requests at shutdown and wait until broadcast is completed as a maker need to remove his offers at shutdown. - Add handling for the case that there are very few connections (as in dev setup). - Make BundleOfEnvelopes extend BroadcastMessage - Add complete handler for broadCaster to shutdown in P2PService and wait with shutdown of other services until broadcaster is completed. - Remove case for repeated shutdown call on P2PService as it cannot happen. --- .../bisq/network/p2p/BundleOfEnvelopes.java | 3 +- .../java/bisq/network/p2p/P2PService.java | 86 ++++++++--------- .../network/p2p/peers/BroadcastHandler.java | 92 +++++++++++-------- .../bisq/network/p2p/peers/Broadcaster.java | 30 +++++- 4 files changed, 126 insertions(+), 85 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java index c57e0d530e..61756c732d 100644 --- a/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java +++ b/p2p/src/main/java/bisq/network/p2p/BundleOfEnvelopes.java @@ -17,6 +17,7 @@ package bisq.network.p2p; +import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.common.app.Capabilities; @@ -36,7 +37,7 @@ import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value -public final class BundleOfEnvelopes extends NetworkEnvelope implements ExtendedDataSizePermission, CapabilityRequiringPayload { +public final class BundleOfEnvelopes extends BroadcastMessage implements ExtendedDataSizePermission, CapabilityRequiringPayload { private final List envelopes; diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 563c3905b7..fd0fa2618d 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -121,7 +121,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); - private volatile boolean shutDownInProgress; private boolean shutDownComplete; private final Subscription networkReadySubscription; private boolean isBootstrapped; @@ -210,48 +209,51 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } public void shutDown(Runnable shutDownCompleteHandler) { - if (!shutDownInProgress) { - shutDownInProgress = true; + shutDownResultHandlers.add(shutDownCompleteHandler); - shutDownResultHandlers.add(shutDownCompleteHandler); - - if (p2PDataStorage != null) - p2PDataStorage.shutDown(); - - if (peerManager != null) - peerManager.shutDown(); - - if (broadcaster != null) - broadcaster.shutDown(); - - if (requestDataManager != null) - requestDataManager.shutDown(); - - if (peerExchangeManager != null) - peerExchangeManager.shutDown(); - - if (keepAliveManager != null) - keepAliveManager.shutDown(); - - if (networkReadySubscription != null) - networkReadySubscription.unsubscribe(); - - if (networkNode != null) { - networkNode.shutDown(() -> { - shutDownResultHandlers.stream().forEach(Runnable::run); - shutDownComplete = true; - }); - } else { - shutDownResultHandlers.stream().forEach(Runnable::run); - shutDownComplete = true; - } + // We need to make sure queued up messages are flushed out before we continue shut down other network + // services + if (broadcaster != null) { + broadcaster.shutDown(this::doShutDown); } else { - log.debug("shutDown already in progress"); - if (shutDownComplete) { - shutDownCompleteHandler.run(); - } else { - shutDownResultHandlers.add(shutDownCompleteHandler); - } + doShutDown(); + } + } + + private void doShutDown() { + log.error("doShutDown"); + if (p2PDataStorage != null) { + p2PDataStorage.shutDown(); + } + + if (peerManager != null) { + peerManager.shutDown(); + } + + if (requestDataManager != null) { + requestDataManager.shutDown(); + } + + if (peerExchangeManager != null) { + peerExchangeManager.shutDown(); + } + + if (keepAliveManager != null) { + keepAliveManager.shutDown(); + } + + if (networkReadySubscription != null) { + networkReadySubscription.unsubscribe(); + } + + if (networkNode != null) { + networkNode.shutDown(() -> { + shutDownResultHandlers.forEach(Runnable::run); + shutDownComplete = true; + }); + } else { + shutDownResultHandlers.forEach(Runnable::run); + shutDownComplete = true; } } @@ -670,6 +672,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override public void onSufficientlyBroadcast(List broadcastRequests) { + log.error("onSufficientlyBroadcast"); broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getMessage() instanceof AddDataMessage) .filter(broadcastRequest -> { @@ -681,6 +684,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) { + log.error("onNotSufficientlyBroadcast"); sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" + "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" + "numOfFailedBroadcast=" + numOfFailedBroadcast); diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index 7ffe569360..cc5b45bc52 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -21,10 +21,10 @@ import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.storage.messages.BroadcastMessage; import bisq.common.Timer; import bisq.common.UserThread; -import bisq.common.proto.network.NetworkEnvelope; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -71,7 +71,7 @@ public class BroadcastHandler implements PeerManager.Listener { private final String uid; private boolean stopped, timeoutTriggered; - private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeers; + private int numOfCompletedBroadcasts, numOfFailedBroadcasts, numPeersForBroadcast; private Timer timeoutTimer; @@ -93,25 +93,31 @@ public class BroadcastHandler implements PeerManager.Listener { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void broadcast(List broadcastRequests) { + public void broadcast(List broadcastRequests, boolean shutDownRequested) { List confirmedConnections = new ArrayList<>(networkNode.getConfirmedConnections()); Collections.shuffle(confirmedConnections); int delay; - if (requestsContainOwnMessage(broadcastRequests)) { - // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and - // with shorter delay - numPeers = confirmedConnections.size(); - delay = 50; + if (shutDownRequested) { + delay = 1; + // We sent to all peers as in case we had offers we want that it gets removed with higher reliability + numPeersForBroadcast = confirmedConnections.size(); } else { - // Relay nodes only send to max 7 peers and with longer delay - numPeers = Math.min(7, confirmedConnections.size()); - delay = 100; + if (requestsContainOwnMessage(broadcastRequests)) { + // The broadcastRequests contains at least 1 message we have originated, so we send to all peers and + // with shorter delay + numPeersForBroadcast = confirmedConnections.size(); + delay = 50; + } else { + // Relay nodes only send to max 7 peers and with longer delay + numPeersForBroadcast = Math.min(7, confirmedConnections.size()); + delay = 100; + } } - setupTimeoutHandler(broadcastRequests, delay); + setupTimeoutHandler(broadcastRequests, delay, shutDownRequested); - int iterations = numPeers; + int iterations = numPeersForBroadcast; for (int i = 0; i < iterations; i++) { long minDelay = (i + 1) * delay; long maxDelay = (i + 2) * delay; @@ -129,8 +135,8 @@ public class BroadcastHandler implements PeerManager.Listener { // Could be empty list... if (broadcastRequestsForConnection.isEmpty()) { // We decrease numPeers in that case for making completion checks correct. - if (numPeers > 0) { - numPeers--; + if (numPeersForBroadcast > 0) { + numPeersForBroadcast--; } checkForCompletion(); return; @@ -139,8 +145,8 @@ public class BroadcastHandler implements PeerManager.Listener { if (connection.isStopped()) { // Connection has died in the meantime. We skip it. // We decrease numPeers in that case for making completion checks correct. - if (numPeers > 0) { - numPeers--; + if (numPeersForBroadcast > 0) { + numPeersForBroadcast--; } checkForCompletion(); return; @@ -188,8 +194,12 @@ public class BroadcastHandler implements PeerManager.Listener { return broadcastRequests.stream().anyMatch(e -> myAddress.equals(e.getSender())); } - private void setupTimeoutHandler(List broadcastRequests, int delay) { - long timeoutDelay = BASE_TIMEOUT_MS + delay * (numPeers + 1); // We added 1 in the loop + private void setupTimeoutHandler(List broadcastRequests, + int delay, + boolean shutDownRequested) { + // In case of shutdown we try to complete fast and set a short 1 second timeout + long baseTimeoutMs = shutDownRequested ? TimeUnit.SECONDS.toMillis(1) : BASE_TIMEOUT_MS; + long timeoutDelay = baseTimeoutMs + delay * (numPeersForBroadcast + 1); // We added 1 in the loop timeoutTimer = UserThread.runAfter(() -> { if (stopped) { return; @@ -197,18 +207,20 @@ public class BroadcastHandler implements PeerManager.Listener { timeoutTriggered = true; - String errorMessage = "Timeout: Broadcast did not complete after " + timeoutDelay + " sec." + "\n" + - "numOfPeers=" + numPeers + "\n" + - "numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n" + - "numOfFailedBroadcasts=" + numOfFailedBroadcasts; - - log.warn(errorMessage); + log.warn("Broadcast did not complete after {} sec.\n" + + "numPeersForBroadcast={}\n" + + "numOfCompletedBroadcasts={}\n" + + "numOfFailedBroadcasts={}", + timeoutDelay / 1000d, + numPeersForBroadcast, + numOfCompletedBroadcasts, + numOfFailedBroadcasts); maybeNotifyListeners(broadcastRequests); cleanup(); - }, timeoutDelay); + }, timeoutDelay, TimeUnit.MILLISECONDS); } // We exclude the requests containing a message we received from that connection @@ -223,8 +235,9 @@ public class BroadcastHandler implements PeerManager.Listener { } private void sendToPeer(Connection connection, List broadcastRequestsForConnection) { - NetworkEnvelope networkEnvelope = getNetworkEnvelope(broadcastRequestsForConnection); - SettableFuture future = networkNode.sendMessage(connection, networkEnvelope); + // Can be BundleOfEnvelopes or a single BroadcastMessage + BroadcastMessage broadcastMessage = getMessage(broadcastRequestsForConnection); + SettableFuture future = networkNode.sendMessage(connection, broadcastMessage); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -236,7 +249,6 @@ public class BroadcastHandler implements PeerManager.Listener { } maybeNotifyListeners(broadcastRequestsForConnection); - checkForCompletion(); } @@ -244,7 +256,6 @@ public class BroadcastHandler implements PeerManager.Listener { public void onFailure(@NotNull Throwable throwable) { log.warn("Broadcast to {} failed. ErrorMessage={}", connection.getPeersNodeAddressOptional(), throwable.getMessage()); - numOfFailedBroadcasts++; if (stopped) { @@ -252,13 +263,12 @@ public class BroadcastHandler implements PeerManager.Listener { } maybeNotifyListeners(broadcastRequestsForConnection); - checkForCompletion(); } }); } - private NetworkEnvelope getNetworkEnvelope(List broadcastRequests) { + private BroadcastMessage getMessage(List broadcastRequests) { if (broadcastRequests.size() == 1) { // If we only have 1 message we avoid the overhead of the BundleOfEnvelopes and send the message directly return broadcastRequests.get(0).getMessage(); @@ -270,22 +280,26 @@ public class BroadcastHandler implements PeerManager.Listener { } private void maybeNotifyListeners(List broadcastRequests) { + int numOfCompletedBroadcastsTarget = Math.max(1, Math.min(numPeersForBroadcast, 3)); // We use equal checks to avoid duplicated listener calls as it would be the case with >= checks. - if (numOfCompletedBroadcasts == 3) { - // We have heard back from 3 peers so we consider the message was sufficiently broadcast. + if (numOfCompletedBroadcasts == numOfCompletedBroadcastsTarget) { + // We have heard back from 3 peers (or all peers if numPeers is lower) so we consider the message was sufficiently broadcast. broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); } else { - int maxPossibleSuccessCases = numPeers - numOfFailedBroadcasts; - if (maxPossibleSuccessCases == 2) { - // We never can reach required resilience as too many numOfFailedBroadcasts occurred. + // Number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. + // Thus we never can reach required resilience as too many numOfFailedBroadcasts occurred. + int openRequests = numPeersForBroadcast - numOfCompletedBroadcasts - numOfFailedBroadcasts; + int maxPossibleSuccessCases = openRequests + numOfCompletedBroadcasts; + // We subtract 1 as we want to have it called only once, with a < comparision we would trigger repeatedly. + if (maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1) { broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); - } else if (timeoutTriggered && numOfCompletedBroadcasts < 3) { + } else if (timeoutTriggered && numOfCompletedBroadcasts < numOfCompletedBroadcastsTarget) { // We did not reach resilience level and timeout prevents to reach it later broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) @@ -296,7 +310,7 @@ public class BroadcastHandler implements PeerManager.Listener { } private void checkForCompletion() { - if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeers) { + if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numPeersForBroadcast) { cleanup(); } } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index a0ef75257a..4abbba55cc 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import lombok.Value; import lombok.extern.slf4j.Slf4j; @@ -46,6 +47,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private final Set broadcastHandlers = new CopyOnWriteArraySet<>(); private final List broadcastRequests = new ArrayList<>(); private Timer timer; + private boolean shutDownRequested; + private Runnable shutDownResultHandler; /////////////////////////////////////////////////////////////////////////////////////////// @@ -58,12 +61,24 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { this.peerManager = peerManager; } - public void shutDown() { - broadcastHandlers.forEach(BroadcastHandler::cancel); + public void shutDown(Runnable resultHandler) { + shutDownRequested = true; + shutDownResultHandler = resultHandler; + if (broadcastRequests.isEmpty()) { + doShutDown(); + } else { + // We set delay of broadcasts and timeout to very low values, + // so we can expect that we get onCompleted called very fast and trigger the doShutDown from there. + maybeBroadcastBundle(); + } + } + private void doShutDown() { + broadcastHandlers.forEach(BroadcastHandler::cancel); if (timer != null) { timer.stop(); } + shutDownResultHandler.run(); } @@ -81,6 +96,8 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener) { broadcastRequests.add(new BroadcastRequest(message, sender, listener)); + // Keep that log on INFO for better debugging if the feature works as expected. Later it can + // be remove or set to DEBUG log.info("Broadcast requested for {}. We queue it up for next bundled broadcast.", message.getClass().getSimpleName()); @@ -91,10 +108,12 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private void maybeBroadcastBundle() { if (!broadcastRequests.isEmpty()) { - log.info("Broadcast bundled requests of {} messages", broadcastRequests.size()); + log.info("Broadcast bundled requests of {} messages. Message types: {}", + broadcastRequests.size(), + broadcastRequests.stream().map(e -> e.getClass().getSimpleName()).collect(Collectors.toList())); BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); broadcastHandlers.add(broadcastHandler); - broadcastHandler.broadcast(new ArrayList<>(broadcastRequests)); + broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested); broadcastRequests.clear(); timer = null; @@ -109,6 +128,9 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { @Override public void onCompleted(BroadcastHandler broadcastHandler) { broadcastHandlers.remove(broadcastHandler); + if (shutDownRequested) { + doShutDown(); + } } From 627052755b137fb246568d9e3db8c175ef9b1046 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 15:47:51 -0500 Subject: [PATCH 21/52] Fix log --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index 4abbba55cc..d1288ffe94 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -110,7 +110,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { if (!broadcastRequests.isEmpty()) { log.info("Broadcast bundled requests of {} messages. Message types: {}", broadcastRequests.size(), - broadcastRequests.stream().map(e -> e.getClass().getSimpleName()).collect(Collectors.toList())); + broadcastRequests.stream().map(e -> e.getMessage().getClass().getSimpleName()).collect(Collectors.toList())); BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); broadcastHandlers.add(broadcastHandler); broadcastHandler.broadcast(new ArrayList<>(broadcastRequests), shutDownRequested); From 21ff2df232f4b2ed229000e9eb93f72471f7710a Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 16:42:55 -0500 Subject: [PATCH 22/52] Remove dev logs --- p2p/src/main/java/bisq/network/p2p/P2PService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index fd0fa2618d..1db22f6f40 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -672,7 +672,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override public void onSufficientlyBroadcast(List broadcastRequests) { - log.error("onSufficientlyBroadcast"); broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getMessage() instanceof AddDataMessage) .filter(broadcastRequest -> { @@ -684,7 +683,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onNotSufficientlyBroadcast(int numOfCompletedBroadcasts, int numOfFailedBroadcast) { - log.error("onNotSufficientlyBroadcast"); sendMailboxMessageListener.onFault("Message was not sufficiently broadcast.\n" + "numOfCompletedBroadcasts: " + numOfCompletedBroadcasts + ".\n" + "numOfFailedBroadcast=" + numOfFailedBroadcast); From 0e8704e74f546eef66de5c26225cb382d4b2e5b6 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 16:50:45 -0500 Subject: [PATCH 23/52] Remove dev logs, remove unread property --- p2p/src/main/java/bisq/network/p2p/P2PService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 1db22f6f40..23a90f8a3a 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -121,7 +121,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); - private boolean shutDownComplete; private final Subscription networkReadySubscription; private boolean isBootstrapped; private final KeepAliveManager keepAliveManager; @@ -221,7 +220,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } private void doShutDown() { - log.error("doShutDown"); if (p2PDataStorage != null) { p2PDataStorage.shutDown(); } @@ -249,11 +247,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis if (networkNode != null) { networkNode.shutDown(() -> { shutDownResultHandlers.forEach(Runnable::run); - shutDownComplete = true; }); } else { shutDownResultHandlers.forEach(Runnable::run); - shutDownComplete = true; } } From 2f291a6acb3b33ce79f9b5283d707e0217a5be31 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 21:23:02 -0500 Subject: [PATCH 24/52] Cleanup Filter value object. Remove nullables from old versions. Keep signatureAsBase64 nullable as it is set null at creation time and set later after signing. We use filter protobuf data as input for signature so we have to set signature to null and apply the signature afterwards. To keep object immutable we clone it with static clone method. Similar we handle signature verification. We clone the filter with a null value for the signature and do the validation with that cloned object. Use now the complete object data for signature creation. We use the protobuf data for creating the signature with DER encoding. We restict ourself more regarding backward compatibility but I think it is not a big problem. If a new field is added to Filter and deployed the maintainer needs to publishes a new filter object. By using the new version he cannot remove the old filter from the network as the protobuf data is different and sig verification on the P2P datastorage level will fail. This is intended to keep the old filter alive for some time to support not updated users. We do not remove invalid filters anymore from our local storage to enable seed nodes to support also old filter objects. For not updated users the new filter will fail at sig verification because the protobuf data is different. So they ignore the new filter and still use the old filter. For updated users the old filter will fail and the new filter is accepted. As it has a newer date it would also replace the old filter anyway. If the maintainer wants to delete the old filter from the network he can use the old app version and remove the filter. It is recommended to keep a copy of the data directory before the update so that the removal of the older filter is possible. Refacorings: - Rename isPeersPaymentAccountDataAreBanned to arePeersPaymentAccountDataBanned - Rename isSignerPubKeyBanned to isWitnessSignerPubKeyBanned --- .../java/bisq/common/proto/ProtoUtil.java | 8 + .../account/sign/SignedWitnessService.java | 4 +- .../witness/AccountAgeWitnessService.java | 8 +- .../main/java/bisq/core/filter/Filter.java | 331 +++++++++------- .../java/bisq/core/filter/FilterManager.java | 373 +++++++++--------- .../trade/protocol/tasks/ApplyFilter.java | 2 +- .../sign/SignedWitnessServiceTest.java | 22 +- .../witness/AccountAgeWitnessServiceTest.java | 4 +- .../main/overlays/windows/FilterWindow.java | 82 ++-- proto/src/main/proto/pb.proto | 1 + 10 files changed, 451 insertions(+), 384 deletions(-) diff --git a/common/src/main/java/bisq/common/proto/ProtoUtil.java b/common/src/main/java/bisq/common/proto/ProtoUtil.java index 6fd7b0f2e5..e605d12e33 100644 --- a/common/src/main/java/bisq/common/proto/ProtoUtil.java +++ b/common/src/main/java/bisq/common/proto/ProtoUtil.java @@ -18,12 +18,15 @@ package bisq.common.proto; import bisq.common.Proto; +import bisq.common.util.CollectionUtils; import com.google.protobuf.ByteString; import com.google.protobuf.Message; +import com.google.protobuf.ProtocolStringList; import com.google.common.base.Enums; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -95,4 +98,9 @@ public class ProtoUtil { public static Iterable collectionToProto(Collection collection, Function extra) { return collection.stream().map(o -> extra.apply(o.toProtoMessage())).collect(Collectors.toList()); } + + public static List protocolStringListToList(ProtocolStringList protocolStringList) { + return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList); + } + } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java index a926045df4..e27bf2493c 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -180,7 +180,7 @@ public class SignedWitnessService { public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getWitnessOwnerPubKey) - .anyMatch(ownerPubKey -> filterManager.isSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); + .anyMatch(ownerPubKey -> filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); } private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { @@ -442,7 +442,7 @@ public class SignedWitnessService { private boolean isValidSignerWitnessInternal(SignedWitness signedWitness, long childSignedWitnessDateMillis, Stack excludedPubKeys) { - if (filterManager.isSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { + if (filterManager.isWitnessSignerPubKeyBanned(Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey()))) { return false; } if (!verifySignature(signedWitness)) { diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index f4620c7d0e..f0256e45d5 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -713,13 +713,13 @@ public class AccountAgeWitnessService { filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) || filterManager.isPaymentMethodBanned( PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) || - filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getBuyerPaymentAccountPayload(), + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload(), new PaymentAccountFilter[1]) || - filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getSellerPaymentAccountPayload(), + filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload(), new PaymentAccountFilter[1]) || - filterManager.isSignerPubKeyBanned( + filterManager.isWitnessSignerPubKeyBanned( Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) || - filterManager.isSignerPubKeyBanned( + filterManager.isWitnessSignerPubKeyBanned( Utils.HEX.encode(dispute.getContract().getSellerPubKeyRing().getSignaturePubKeyBytes())); return !isFiltered; } diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 0c2e39b095..9698f8ae3e 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -21,111 +21,182 @@ import bisq.network.p2p.storage.payload.ExpirablePayload; import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.common.crypto.Sig; +import bisq.common.proto.ProtoUtil; import bisq.common.util.CollectionUtils; import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; import com.google.protobuf.ByteString; -import com.google.common.annotations.VisibleForTesting; - import java.security.PublicKey; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; +import lombok.Value; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j -@Getter -@EqualsAndHashCode -@ToString +@Value public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final List bannedOfferIds; private final List bannedNodeAddress; private final List bannedPaymentAccounts; - - // Because we added those fields in v 0.5.4 and old versions do not have it we annotate it with @Nullable - @Nullable private final List bannedCurrencies; - @Nullable private final List bannedPaymentMethods; - - // added in v0.6.0 - @Nullable private final List arbitrators; - @Nullable private final List seedNodes; - @Nullable private final List priceRelayNodes; private final boolean preventPublicBtcNetwork; - - // added in v0.6.2 - @Nullable private final List btcNodes; + // SignatureAsBase64 is not set initially as we use the serialized data for signing. We set it after signature is + // created by cloning the object with a non-null sig. + @Nullable + private final String signatureAsBase64; + // The pub key used for the data protection in teh p2p storage + private final byte[] ownerPubKeyBytes; + private final boolean disableDao; + private final String disableDaoBelowVersion; + private final String disableTradeBelowVersion; + private final List mediators; + private final List refundAgents; + private final List bannedSignerPubKeys; + + private final List btcFeeReceiverAddresses; + + private final long creationDate; - private String signatureAsBase64; - private byte[] ownerPubKeyBytes; // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @Nullable private Map extraDataMap; - private PublicKey ownerPubKey; - // added in v0.9.4 - private final boolean disableDao; + private transient PublicKey ownerPubKey; - // added in v0.9.8 - @Nullable - private final String disableDaoBelowVersion; - @Nullable - private final String disableTradeBelowVersion; + // After we have created the signature from the filter data we clone it and apply the signature + static Filter cloneWithSig(Filter filter, String signatureAsBase64) { + return new Filter(filter.getBannedOfferIds(), + filter.getBannedNodeAddress(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + signatureAsBase64); + } - // added in v1.1.6 - @Nullable - private final List mediators; - - // added in v1.2.0 - @Nullable - private final List refundAgents; - - // added in v1.2.x - @Nullable - private final List bannedSignerPubKeys; - - // added in v1.3.2 - @Nullable - private final List btcFeeReceiverAddresses; + // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again + static Filter cloneWithoutSig(Filter filter) { + return new Filter(filter.getBannedOfferIds(), + filter.getBannedNodeAddress(), + filter.getBannedPaymentAccounts(), + filter.getBannedCurrencies(), + filter.getBannedPaymentMethods(), + filter.getArbitrators(), + filter.getSeedNodes(), + filter.getPriceRelayNodes(), + filter.isPreventPublicBtcNetwork(), + filter.getBtcNodes(), + filter.isDisableDao(), + filter.getDisableDaoBelowVersion(), + filter.getDisableTradeBelowVersion(), + filter.getMediators(), + filter.getRefundAgents(), + filter.getBannedSignerPubKeys(), + filter.getBtcFeeReceiverAddresses(), + filter.getOwnerPubKeyBytes(), + filter.getCreationDate(), + filter.getExtraDataMap(), + null); + } public Filter(List bannedOfferIds, List bannedNodeAddress, List bannedPaymentAccounts, - @Nullable List bannedCurrencies, - @Nullable List bannedPaymentMethods, - @Nullable List arbitrators, - @Nullable List seedNodes, - @Nullable List priceRelayNodes, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, boolean preventPublicBtcNetwork, - @Nullable List btcNodes, + List btcNodes, boolean disableDao, - @Nullable String disableDaoBelowVersion, - @Nullable String disableTradeBelowVersion, - @Nullable List mediators, - @Nullable List refundAgents, - @Nullable List bannedSignerPubKeys, - @Nullable List btcFeeReceiverAddresses) { + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedSignerPubKeys, + List btcFeeReceiverAddresses, + PublicKey ownerPubKey) { + this(bannedOfferIds, + bannedNodeAddress, + bannedPaymentAccounts, + bannedCurrencies, + bannedPaymentMethods, + arbitrators, + seedNodes, + priceRelayNodes, + preventPublicBtcNetwork, + btcNodes, + disableDao, + disableDaoBelowVersion, + disableTradeBelowVersion, + mediators, + refundAgents, + bannedSignerPubKeys, + btcFeeReceiverAddresses, + Sig.getPublicKeyBytes(ownerPubKey), + System.currentTimeMillis(), + null, + null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private Filter(List bannedOfferIds, + List bannedNodeAddress, + List bannedPaymentAccounts, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, + boolean preventPublicBtcNetwork, + List btcNodes, + boolean disableDao, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedSignerPubKeys, + List btcFeeReceiverAddresses, + byte[] ownerPubKeyBytes, + long creationDate, + @Nullable Map extraDataMap, + @Nullable String signatureAsBase64) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -143,116 +214,74 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.refundAgents = refundAgents; this.bannedSignerPubKeys = bannedSignerPubKeys; this.btcFeeReceiverAddresses = btcFeeReceiverAddresses; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @VisibleForTesting - public Filter(List bannedOfferIds, - List bannedNodeAddress, - List bannedPaymentAccounts, - @Nullable List bannedCurrencies, - @Nullable List bannedPaymentMethods, - @Nullable List arbitrators, - @Nullable List seedNodes, - @Nullable List priceRelayNodes, - boolean preventPublicBtcNetwork, - @Nullable List btcNodes, - boolean disableDao, - @Nullable String disableDaoBelowVersion, - @Nullable String disableTradeBelowVersion, - String signatureAsBase64, - byte[] ownerPubKeyBytes, - @Nullable Map extraDataMap, - @Nullable List mediators, - @Nullable List refundAgents, - @Nullable List bannedSignerPubKeys, - @Nullable List btcFeeReceiverAddresses) { - this(bannedOfferIds, - bannedNodeAddress, - bannedPaymentAccounts, - bannedCurrencies, - bannedPaymentMethods, - arbitrators, - seedNodes, - priceRelayNodes, - preventPublicBtcNetwork, - btcNodes, - disableDao, - disableDaoBelowVersion, - disableTradeBelowVersion, - mediators, - refundAgents, - bannedSignerPubKeys, - btcFeeReceiverAddresses); - this.signatureAsBase64 = signatureAsBase64; this.ownerPubKeyBytes = ownerPubKeyBytes; + this.creationDate = creationDate; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + this.signatureAsBase64 = signatureAsBase64; ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); } @Override public protobuf.StoragePayload toProtoMessage() { - checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); - checkNotNull(ownerPubKeyBytes, "ownerPubKeyBytes must not be null"); List paymentAccountFilterList = bannedPaymentAccounts.stream() .map(PaymentAccountFilter::toProtoMessage) .collect(Collectors.toList()); - final protobuf.Filter.Builder builder = protobuf.Filter.newBuilder() - .addAllBannedOfferIds(bannedOfferIds) + + protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds) .addAllBannedNodeAddress(bannedNodeAddress) .addAllBannedPaymentAccounts(paymentAccountFilterList) - .setSignatureAsBase64(signatureAsBase64) - .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .addAllBannedCurrencies(bannedCurrencies) + .addAllBannedPaymentMethods(bannedPaymentMethods) + .addAllArbitrators(arbitrators) + .addAllSeedNodes(seedNodes) + .addAllPriceRelayNodes(priceRelayNodes) .setPreventPublicBtcNetwork(preventPublicBtcNetwork) - .setDisableDao(disableDao); + .addAllBtcNodes(btcNodes) + .setDisableDao(disableDao) + .setDisableDaoBelowVersion(disableDaoBelowVersion) + .setDisableTradeBelowVersion(disableTradeBelowVersion) + .addAllMediators(mediators) + .addAllRefundAgents(refundAgents) + .addAllBannedSignerPubKeys(bannedSignerPubKeys) + .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) + .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .setCreationDate(creationDate); - Optional.ofNullable(bannedCurrencies).ifPresent(builder::addAllBannedCurrencies); - Optional.ofNullable(bannedPaymentMethods).ifPresent(builder::addAllBannedPaymentMethods); - Optional.ofNullable(arbitrators).ifPresent(builder::addAllArbitrators); - Optional.ofNullable(seedNodes).ifPresent(builder::addAllSeedNodes); - Optional.ofNullable(priceRelayNodes).ifPresent(builder::addAllPriceRelayNodes); - Optional.ofNullable(btcNodes).ifPresent(builder::addAllBtcNodes); - Optional.ofNullable(disableDaoBelowVersion).ifPresent(builder::setDisableDaoBelowVersion); - Optional.ofNullable(disableTradeBelowVersion).ifPresent(builder::setDisableTradeBelowVersion); + Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); - Optional.ofNullable(mediators).ifPresent(builder::addAllMediators); - Optional.ofNullable(refundAgents).ifPresent(builder::addAllRefundAgents); - Optional.ofNullable(bannedSignerPubKeys).ifPresent(builder::addAllBannedSignerPubKeys); - Optional.ofNullable(btcFeeReceiverAddresses).ifPresent(builder::addAllBtcFeeReceiverAddresses); return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); } public static Filter fromProto(protobuf.Filter proto) { - return new Filter(new ArrayList<>(proto.getBannedOfferIdsList()), - new ArrayList<>(proto.getBannedNodeAddressList()), - proto.getBannedPaymentAccountsList().stream() - .map(PaymentAccountFilter::fromProto) - .collect(Collectors.toList()), - CollectionUtils.isEmpty(proto.getBannedCurrenciesList()) ? null : new ArrayList<>(proto.getBannedCurrenciesList()), - CollectionUtils.isEmpty(proto.getBannedPaymentMethodsList()) ? null : new ArrayList<>(proto.getBannedPaymentMethodsList()), - CollectionUtils.isEmpty(proto.getArbitratorsList()) ? null : new ArrayList<>(proto.getArbitratorsList()), - CollectionUtils.isEmpty(proto.getSeedNodesList()) ? null : new ArrayList<>(proto.getSeedNodesList()), - CollectionUtils.isEmpty(proto.getPriceRelayNodesList()) ? null : new ArrayList<>(proto.getPriceRelayNodesList()), + List bannedPaymentAccountsList = proto.getBannedPaymentAccountsList().stream() + .map(PaymentAccountFilter::fromProto) + .collect(Collectors.toList()); + + + return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()), + ProtoUtil.protocolStringListToList(proto.getBannedNodeAddressList()), + bannedPaymentAccountsList, + ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()), + ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()), + ProtoUtil.protocolStringListToList(proto.getArbitratorsList()), + ProtoUtil.protocolStringListToList(proto.getSeedNodesList()), + ProtoUtil.protocolStringListToList(proto.getPriceRelayNodesList()), proto.getPreventPublicBtcNetwork(), - CollectionUtils.isEmpty(proto.getBtcNodesList()) ? null : new ArrayList<>(proto.getBtcNodesList()), + ProtoUtil.protocolStringListToList(proto.getBtcNodesList()), proto.getDisableDao(), - proto.getDisableDaoBelowVersion().isEmpty() ? null : proto.getDisableDaoBelowVersion(), - proto.getDisableTradeBelowVersion().isEmpty() ? null : proto.getDisableTradeBelowVersion(), - proto.getSignatureAsBase64(), + proto.getDisableDaoBelowVersion(), + proto.getDisableTradeBelowVersion(), + ProtoUtil.protocolStringListToList(proto.getMediatorsList()), + ProtoUtil.protocolStringListToList(proto.getRefundAgentsList()), + ProtoUtil.protocolStringListToList(proto.getBannedSignerPubKeysList()), + ProtoUtil.protocolStringListToList(proto.getBtcFeeReceiverAddressesList()), proto.getOwnerPubKeyBytes().toByteArray(), + proto.getCreationDate(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), - CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList()), - CollectionUtils.isEmpty(proto.getRefundAgentsList()) ? null : new ArrayList<>(proto.getRefundAgentsList()), - CollectionUtils.isEmpty(proto.getBannedSignerPubKeysList()) ? - null : new ArrayList<>(proto.getBannedSignerPubKeysList()), - CollectionUtils.isEmpty(proto.getBtcFeeReceiverAddressesList()) ? null : - new ArrayList<>(proto.getBtcFeeReceiverAddressesList())); + proto.getSignatureAsBase64() + ); } @@ -265,13 +294,6 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { return TimeUnit.DAYS.toMillis(180); } - void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { - this.signatureAsBase64 = signatureAsBase64; - this.ownerPubKey = ownerPubKey; - - ownerPubKeyBytes = Sig.getPublicKeyBytes(this.ownerPubKey); - } - @Override public String toString() { return "Filter{" + @@ -285,7 +307,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n priceRelayNodes=" + priceRelayNodes + ",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork + ",\n btcNodes=" + btcNodes + - ",\n extraDataMap=" + extraDataMap + + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' + + ",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) + ",\n disableDao=" + disableDao + ",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' + ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + @@ -293,6 +316,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n refundAgents=" + refundAgents + ",\n bannedSignerPubKeys=" + bannedSignerPubKeys + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + + ",\n creationDate=" + creationDate + + ",\n extraDataMap=" + extraDataMap + "\n}"; } } diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index f903638532..7b54dc0566 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -29,9 +29,7 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PServiceListener; import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; -import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.app.Version; import bisq.common.config.Config; @@ -39,6 +37,7 @@ import bisq.common.config.ConfigFileEditor; import bisq.common.crypto.KeyRing; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Utils; import javax.inject.Inject; @@ -47,33 +46,35 @@ import javax.inject.Named; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import java.security.SignatureException; +import org.spongycastle.util.encoders.Base64; + +import java.security.PublicKey; + +import java.nio.charset.StandardCharsets; import java.math.BigInteger; -import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; import java.lang.reflect.Method; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkNotNull; import static org.bitcoinj.core.Utils.HEX; +/** + * We only support one active filter, if we receive multiple we use the one with the more recent creationDate. + */ +@Slf4j public class FilterManager { - - private static final Logger log = LoggerFactory.getLogger(FilterManager.class); - - public static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; - public static final String BANNED_SEED_NODES = "bannedSeedNodes"; - public static final String BANNED_BTC_NODES = "bannedBtcNodes"; + private static final String BANNED_PRICE_RELAY_NODES = "bannedPriceRelayNodes"; + private static final String BANNED_SEED_NODES = "bannedSeedNodes"; + private static final String BANNED_BTC_NODES = "bannedBtcNodes"; /////////////////////////////////////////////////////////////////////////////////////////// @@ -90,16 +91,16 @@ public class FilterManager { private final Preferences preferences; private final ConfigFileEditor configFileEditor; private final ProvidersRepository providersRepository; - private boolean ignoreDevMsg; + private final boolean ignoreDevMsg; private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); private final List listeners = new CopyOnWriteArrayList<>(); - private final String pubKeyAsHex; + private ECKey filterSigningKey; /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, Initialization + // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @Inject @@ -118,48 +119,55 @@ public class FilterManager { this.configFileEditor = new ConfigFileEditor(config.configFile); this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; + pubKeyAsHex = useDevPrivilegeKeys ? DevEnv.DEV_PRIVILEGE_PUB_KEY : "022ac7b7766b0aedff82962522c2c14fb8d1961dabef6e5cfd10edc679456a32f1"; } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + public void onAllServicesInitialized() { - if (!ignoreDevMsg) { - - final List list = new ArrayList<>(p2PService.getP2PDataStorage().getMap().values()); - list.forEach(e -> { - final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload(); - if (protectedStoragePayload instanceof Filter) - addFilter((Filter) protectedStoragePayload); - }); - - p2PService.addHashSetChangedListener(new HashMapChangedListener() { - @Override - public void onAdded(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) { - Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - boolean wasValid = addFilter(filter); - if (!wasValid) { - UserThread.runAfter(() -> p2PService.getP2PDataStorage().removeInvalidProtectedStorageEntry(protectedStorageEntry), 1); - } - } - }); - } - - @Override - public void onRemoved(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) { - Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (verifySignature(filter) && getFilter().equals(filter)) - resetFilters(); - } - }); - } - }); + if (ignoreDevMsg) { + return; } + p2PService.getP2PDataStorage().getMap().values().stream() + .map(ProtectedStorageEntry::getProtectedStoragePayload) + .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) + .map(protectedStoragePayload -> (Filter) protectedStoragePayload) + .filter(this::verifySignature) + .forEach(this::onFilterAddedFromNetwork); + + p2PService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + if (verifySignature(filter)) { + onFilterAddedFromNetwork(filter); + } + }); + } + + @Override + public void onRemoved(Collection protectedStorageEntries) { + protectedStorageEntries.stream() + .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) + .forEach(protectedStorageEntry -> { + Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); + if (verifySignature(filter)) { + onFilterRemovedFromNetwork(filter); + } + }); + } + }); + p2PService.addP2PServiceListener(new P2PServiceListener() { @Override public void onDataReceived() { @@ -175,12 +183,12 @@ public class FilterManager { @Override public void onUpdatedDataReceived() { - // We should have received all data at that point and if the filers were not set we - // clean up as it might be that we missed the filter remove message if we have not been online. - UserThread.runAfter(() -> { - if (filterProperty.get() == null) - resetFilters(); - }, 1); + // We should have received all data at that point and if the filters were not set we + // clean up the persisted banned nodes in the options file as it might be that we missed the filter + // remove message if we have not been online. + if (filterProperty.get() == null) { + clearBannedNodes(); + } } @Override @@ -201,54 +209,37 @@ public class FilterManager { }); } - private void resetFilters() { - saveBannedNodes(BANNED_BTC_NODES, null); - saveBannedNodes(BANNED_SEED_NODES, null); - saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); - - if (providersRepository.getBannedNodes() != null) - providersRepository.applyBannedNodes(null); - - filterProperty.set(null); - } - - private boolean addFilter(Filter filter) { - if (verifySignature(filter)) { - // Seed nodes are requested at startup before we get the filter so we only apply the banned - // nodes at the next startup and don't update the list in the P2P network domain. - // We persist it to the property file which is read before any other initialisation. - saveBannedNodes(BANNED_SEED_NODES, filter.getSeedNodes()); - saveBannedNodes(BANNED_BTC_NODES, filter.getBtcNodes()); - - // Banned price relay nodes we can apply at runtime - final List priceRelayNodes = filter.getPriceRelayNodes(); - saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); - - providersRepository.applyBannedNodes(priceRelayNodes); - - filterProperty.set(filter); - listeners.forEach(e -> e.onFilterAdded(filter)); - - if (filter.isPreventPublicBtcNetwork() && - preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) - preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); - return true; - } else { + public boolean isValidDevPrivilegeKey(String privKeyString) { + try { + filterSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + return pubKeyAsHex.equals(Utils.HEX.encode(filterSigningKey.getPubKey())); + } catch (Throwable t) { return false; } } - private void saveBannedNodes(String optionName, List bannedNodes) { - if (bannedNodes != null) - configFileEditor.setOption(optionName, String.join(",", bannedNodes)); - else - configFileEditor.clearOption(optionName); + public void publishFilter(Filter filterWithoutSig) { + String signatureAsBase64 = getSignature(filterWithoutSig); + Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); + user.setDevelopersFilter(filterWithSig); + + p2PService.addProtectedStorageEntry(filterWithSig); } - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// + public void removeFilter() { + Filter filterWithSig = user.getDevelopersFilter(); + if (filterWithSig == null) { + // Should not happen as UI button is deactivated in that case + return; + } + + if (p2PService.removeData(filterWithSig)) { + user.setDevelopersFilter(null); + } else { + log.warn("Removing dev filter from network failed"); + } + } public void addListener(Listener listener) { listeners.add(listener); @@ -263,80 +254,6 @@ public class FilterManager { return filterProperty.get(); } - public boolean addFilterMessageIfKeyIsValid(Filter filter, String privKeyString) { - // if there is a previous message we remove that first - if (user.getDevelopersFilter() != null) - removeFilterMessageIfKeyIsValid(privKeyString); - - boolean isKeyValid = isKeyValid(privKeyString); - if (isKeyValid) { - signAndAddSignatureToFilter(filter); - user.setDevelopersFilter(filter); - - boolean result = p2PService.addProtectedStorageEntry(filter); - if (result) - log.trace("Add filter to network was successful. FilterMessage = {}", filter); - - } - return isKeyValid; - } - - public boolean removeFilterMessageIfKeyIsValid(String privKeyString) { - if (isKeyValid(privKeyString)) { - Filter filter = user.getDevelopersFilter(); - if (filter == null) { - log.warn("Developers filter is null"); - } else if (p2PService.removeData(filter)) { - log.trace("Remove filter from network was successful. FilterMessage = {}", filter); - user.setDevelopersFilter(null); - } else { - log.warn("Filter remove failed"); - } - return true; - } else { - return false; - } - } - - private boolean isKeyValid(String privKeyString) { - try { - filterSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); - return pubKeyAsHex.equals(Utils.HEX.encode(filterSigningKey.getPubKey())); - } catch (Throwable t) { - return false; - } - } - - private void signAndAddSignatureToFilter(Filter filter) { - filter.setSigAndPubKey(filterSigningKey.signMessage(getHexFromData(filter)), keyRing.getSignatureKeyPair().getPublic()); - } - - private boolean verifySignature(Filter filter) { - try { - ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(getHexFromData(filter), filter.getSignatureAsBase64()); - return true; - } catch (SignatureException e) { - log.warn("verifySignature failed. filter={}", filter); - return false; - } - } - - // We don't use full data from Filter as we are only interested in the filter data not the sig and keys - private String getHexFromData(Filter filter) { - protobuf.Filter.Builder builder = protobuf.Filter.newBuilder() - .addAllBannedOfferIds(filter.getBannedOfferIds()) - .addAllBannedNodeAddress(filter.getBannedNodeAddress()) - .addAllBannedPaymentAccounts(filter.getBannedPaymentAccounts().stream() - .map(PaymentAccountFilter::toProtoMessage) - .collect(Collectors.toList())); - - Optional.ofNullable(filter.getBannedCurrencies()).ifPresent(builder::addAllBannedCurrencies); - Optional.ofNullable(filter.getBannedPaymentMethods()).ifPresent(builder::addAllBannedPaymentMethods); - Optional.ofNullable(filter.getBannedSignerPubKeys()).ifPresent(builder::addAllBannedSignerPubKeys); - - return Utils.HEX.encode(builder.build().toByteArray()); - } - @Nullable public Filter getDevelopersFilter() { return user.getDevelopersFilter(); @@ -396,8 +313,8 @@ public class FilterManager { return requireUpdateToNewVersion; } - public boolean isPeersPaymentAccountDataAreBanned(PaymentAccountPayload paymentAccountPayload, - PaymentAccountFilter[] appliedPaymentAccountFilter) { + public boolean arePeersPaymentAccountDataBanned(PaymentAccountPayload paymentAccountPayload, + PaymentAccountFilter[] appliedPaymentAccountFilter) { return getFilter() != null && getFilter().getBannedPaymentAccounts().stream() .anyMatch(paymentAccountFilter -> { @@ -419,11 +336,115 @@ public class FilterManager { }); } - public boolean isSignerPubKeyBanned(String signerPubKeyAsHex) { + public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) { return getFilter() != null && getFilter().getBannedSignerPubKeys() != null && getFilter().getBannedSignerPubKeys().stream() - .anyMatch(e -> e.equals(signerPubKeyAsHex)); + .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); } + public PublicKey getOwnerPubKey() { + return keyRing.getSignatureKeyPair().getPublic(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onFilterAddedFromNetwork(Filter filter) { + if (filterProperty.get() != null && filterProperty.get().getCreationDate() > filter.getCreationDate()) { + log.warn("We received a new filter from the network but the creation date is older than the " + + "filter we have already. We ignore the new filter.\n" + + "New filer={}\n" + + "Old filter={}", + filter, filterProperty.get()); + return; + } + + // Our new filter is newer so we apply it. + // We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key + // for deploying filters and this is only in place to avoid unintended situations of multiple filters + // from multiple devs or if same dev publishes new filter from different app without the persisted devFilter. + filterProperty.set(filter); + + // Seed nodes are requested at startup before we get the filter so we only apply the banned + // nodes at the next startup and don't update the list in the P2P network domain. + // We persist it to the property file which is read before any other initialisation. + saveBannedNodes(BANNED_SEED_NODES, filter.getSeedNodes()); + saveBannedNodes(BANNED_BTC_NODES, filter.getBtcNodes()); + + // Banned price relay nodes we can apply at runtime + List priceRelayNodes = filter.getPriceRelayNodes(); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); + providersRepository.applyBannedNodes(priceRelayNodes); + + if (filter.isPreventPublicBtcNetwork() && + preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) { + preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); + } + + listeners.forEach(e -> e.onFilterAdded(filter)); + } + + // We clean up potentially banned nodes and set value of filter property to null + private void onFilterRemovedFromNetwork(Filter filter) { + if (!filterProperty.get().equals(filter)) { + return; + } + + clearBannedNodes(); + + if (filter.equals(user.getDevelopersFilter())) { + user.setDevelopersFilter(null); + } + filterProperty.set(null); + } + + // Clears options files from banned nodes + private void clearBannedNodes() { + saveBannedNodes(BANNED_BTC_NODES, null); + saveBannedNodes(BANNED_SEED_NODES, null); + saveBannedNodes(BANNED_PRICE_RELAY_NODES, null); + + if (providersRepository.getBannedNodes() != null) { + providersRepository.applyBannedNodes(null); + } + } + + private void saveBannedNodes(String optionName, List bannedNodes) { + if (bannedNodes != null) + configFileEditor.setOption(optionName, String.join(",", bannedNodes)); + else + configFileEditor.clearOption(optionName); + } + + private String getSignature(Filter filterWithoutSig) { + Sha256Hash hash = getSha256Hash(filterWithoutSig); + ECKey.ECDSASignature ecdsaSignature = filterSigningKey.sign(hash); + byte[] encodeToDER = ecdsaSignature.encodeToDER(); + return new String(Base64.encode(encodeToDER), StandardCharsets.UTF_8); + } + + private boolean verifySignature(Filter filter) { + try { + Filter filterForSigVerification = Filter.cloneWithoutSig(filter); + Sha256Hash hash = getSha256Hash(filterForSigVerification); + + checkNotNull(filter.getSignatureAsBase64(), "filter.getSignatureAsBase64() must not be null"); + byte[] sigData = Base64.decode(filter.getSignatureAsBase64()); + ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(sigData); + + ECKey ecPubKey = ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)); + return ecPubKey.verify(hash, ecdsaSignature); + } catch (Throwable e) { + log.warn("verifySignature failed. filter={}", filter); + return false; + } + } + + private Sha256Hash getSha256Hash(Filter filter) { + byte[] filterData = filter.toProtoMessage().toByteArray(); + return Sha256Hash.of(filterData); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java index a892cb4c11..55e40c120f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java @@ -59,7 +59,7 @@ public class ApplyFilter extends TradeTask { } else if (filterManager.isPaymentMethodBanned(trade.getOffer().getPaymentMethod())) { failed("Payment method is banned.\n" + "Payment method=" + trade.getOffer().getPaymentMethod().getId()); - } else if (filterManager.isPeersPaymentAccountDataAreBanned(paymentAccountPayload, appliedPaymentAccountFilter)) { + } else if (filterManager.arePeersPaymentAccountDataBanned(paymentAccountPayload, appliedPaymentAccountFilter)) { failed("Other trader is banned by their trading account data.\n" + "paymentAccountPayload=" + paymentAccountPayload.getPaymentDetails() + "\n" + "banFilter=" + appliedPaymentAccountFilter[0].toString()); diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java index 1091548315..338a9b3a70 100644 --- a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java @@ -397,14 +397,14 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3); // Second account is banned, first account is still a signer but the other two are no longer signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First account is banned, no accounts in the tree below it are signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); @@ -434,14 +434,14 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3); // Only second account is banned, first account is still a signer but the other two are no longer signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Only first account is banned, account2 and account3 are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(false); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); @@ -484,21 +484,21 @@ public class SignedWitnessServiceTest { signedWitnessService.addToMap(sw3p); // First account is banned, the other two are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // Second account is banned, the other two are still signers - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertTrue(signedWitnessService.isSignerAccountAgeWitness(aew3)); // First and second account is banned, the third is no longer a signer - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); - when(filterManager.isSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner1PubKey))).thenReturn(true); + when(filterManager.isWitnessSignerPubKeyBanned(Utilities.bytesAsHexString(witnessOwner2PubKey))).thenReturn(true); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew1)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew2)); assertFalse(signedWitnessService.isSignerAccountAgeWitness(aew3)); diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 36d52df74d..b37a47d057 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -218,8 +218,8 @@ public class AccountAgeWitnessServiceTest { when(filterManager.isNodeAddressBanned(any())).thenReturn(false); when(filterManager.isCurrencyBanned(any())).thenReturn(false); when(filterManager.isPaymentMethodBanned(any())).thenReturn(false); - when(filterManager.isPeersPaymentAccountDataAreBanned(any(), any())).thenReturn(false); - when(filterManager.isSignerPubKeyBanned(any())).thenReturn(false); + when(filterManager.arePeersPaymentAccountDataBanned(any(), any())).thenReturn(false); + when(filterManager.isWitnessSignerPubKeyBanned(any())).thenReturn(false); when(chargeBackRisk.hasChargebackRisk(any(), any())).thenReturn(true); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 50dbf04f18..8baea00fda 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -160,42 +160,54 @@ public class FilterWindow extends Overlay { disableDaoBelowVersionInputTextField.setText(filter.getDisableDaoBelowVersion()); disableTradeBelowVersionInputTextField.setText(filter.getDisableTradeBelowVersion()); } - Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); - sendButton.setOnAction(e -> { - if (filterManager.addFilterMessageIfKeyIsValid( - new Filter( - readAsList(offerIdsInputTextField), - readAsList(nodesInputTextField), - readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), - readAsList(bannedCurrenciesInputTextField), - readAsList(bannedPaymentMethodsInputTextField), - readAsList(arbitratorsInputTextField), - readAsList(seedNodesInputTextField), - readAsList(priceRelayNodesInputTextField), - preventPublicBtcNetworkCheckBox.isSelected(), - readAsList(btcNodesInputTextField), - disableDaoCheckBox.isSelected(), - disableDaoBelowVersionInputTextField.getText(), - disableTradeBelowVersionInputTextField.getText(), - readAsList(mediatorsInputTextField), - readAsList(refundAgentsInputTextField), - readAsList(bannedSignerPubKeysInputTextField), - readAsList(btcFeeReceiverAddressesInputTextField) - ), - keyInputTextField.getText()) - ) - hide(); - else - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); - }); Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); + removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); + + Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); + sendButton.setOnAction(e -> { + if (keyInputTextField.getText().isEmpty()) { + return; + } + Filter newFilter = new Filter( + readAsList(offerIdsInputTextField), + readAsList(nodesInputTextField), + readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), + readAsList(bannedCurrenciesInputTextField), + readAsList(bannedPaymentMethodsInputTextField), + readAsList(arbitratorsInputTextField), + readAsList(seedNodesInputTextField), + readAsList(priceRelayNodesInputTextField), + preventPublicBtcNetworkCheckBox.isSelected(), + readAsList(btcNodesInputTextField), + disableDaoCheckBox.isSelected(), + disableDaoBelowVersionInputTextField.getText(), + disableTradeBelowVersionInputTextField.getText(), + readAsList(mediatorsInputTextField), + readAsList(refundAgentsInputTextField), + readAsList(bannedSignerPubKeysInputTextField), + readAsList(btcFeeReceiverAddressesInputTextField), + filterManager.getOwnerPubKey() + ); + if (filterManager.isValidDevPrivilegeKey(keyInputTextField.getText())) { + filterManager.publishFilter(newFilter); + removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); + hide(); + } else { + new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + } + }); + removeFilterMessageButton.setOnAction(e -> { - if (keyInputTextField.getText().length() > 0) { - if (filterManager.removeFilterMessageIfKeyIsValid(keyInputTextField.getText())) - hide(); - else - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + if (keyInputTextField.getText().isEmpty()) { + return; + } + + if (filterManager.isValidDevPrivilegeKey(keyInputTextField.getText())) { + filterManager.removeFilter(); + hide(); + } else { + new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); } }); @@ -215,13 +227,13 @@ public class FilterWindow extends Overlay { private void setupFieldFromList(InputTextField field, List values) { if (values != null) - field.setText(values.stream().collect(Collectors.joining(", "))); + field.setText(String.join(", ", values)); } private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List values) { if (values != null) { StringBuilder sb = new StringBuilder(); - values.stream().forEach(e -> { + values.forEach(e -> { if (e != null && e.getPaymentMethodId() != null) { sb .append(e.getPaymentMethodId()) diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 117dbb4cb5..75b73eb736 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -638,6 +638,7 @@ message Filter { repeated string refundAgents = 18; repeated string bannedSignerPubKeys = 19; repeated string btc_fee_receiver_addresses = 20; + int64 creation_date = 21; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older From 28a665e02d8b86ca812611fcfe50034d0a014deb Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 27 Aug 2020 21:29:30 -0500 Subject: [PATCH 25/52] Satisfy annoying Codacy bot --- p2p/src/main/java/bisq/network/p2p/P2PService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 23a90f8a3a..3b0c188e27 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -444,6 +444,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onRemoved(Collection protectedStorageEntries) { + // not used } /////////////////////////////////////////////////////////////////////////////////////////// From 9e4e800cb62e40cb53af57932ce767e775e4a7cc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 12:53:35 -0500 Subject: [PATCH 26/52] Fix tests --- .../main/java/bisq/core/filter/Filter.java | 52 +++++++++++-------- .../core/user/UserPayloadModelVOTest.java | 14 ++--- .../core/util/FeeReceiverSelectorTest.java | 27 ++++++++-- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 9698f8ae3e..93744ef9e9 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -28,6 +28,8 @@ import bisq.common.util.Utilities; import com.google.protobuf.ByteString; +import com.google.common.annotations.VisibleForTesting; + import java.security.PublicKey; import java.util.List; @@ -176,27 +178,28 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private Filter(List bannedOfferIds, - List bannedNodeAddress, - List bannedPaymentAccounts, - List bannedCurrencies, - List bannedPaymentMethods, - List arbitrators, - List seedNodes, - List priceRelayNodes, - boolean preventPublicBtcNetwork, - List btcNodes, - boolean disableDao, - String disableDaoBelowVersion, - String disableTradeBelowVersion, - List mediators, - List refundAgents, - List bannedSignerPubKeys, - List btcFeeReceiverAddresses, - byte[] ownerPubKeyBytes, - long creationDate, - @Nullable Map extraDataMap, - @Nullable String signatureAsBase64) { + @VisibleForTesting + public Filter(List bannedOfferIds, + List bannedNodeAddress, + List bannedPaymentAccounts, + List bannedCurrencies, + List bannedPaymentMethods, + List arbitrators, + List seedNodes, + List priceRelayNodes, + boolean preventPublicBtcNetwork, + List btcNodes, + boolean disableDao, + String disableDaoBelowVersion, + String disableTradeBelowVersion, + List mediators, + List refundAgents, + List bannedSignerPubKeys, + List btcFeeReceiverAddresses, + byte[] ownerPubKeyBytes, + long creationDate, + @Nullable Map extraDataMap, + @Nullable String signatureAsBase64) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -219,7 +222,12 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.signatureAsBase64 = signatureAsBase64; - ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + // ownerPubKeyBytes can be null when called from tests + if (ownerPubKeyBytes != null) { + ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); + } else { + ownerPubKey = null; + } } @Override diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 67d09bb658..de685c9ba4 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -53,13 +53,15 @@ public class UserPayloadModelVOTest { false, null, null, - "string", - new byte[]{10, 0, 0}, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), null, - Lists.newArrayList(), - Lists.newArrayList(), - Lists.newArrayList(), - Lists.newArrayList())); + 0, + null, + null)); + vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index c340c36ecd..2f059edd84 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -22,6 +22,7 @@ import bisq.core.dao.governance.param.Param; import bisq.core.filter.Filter; import bisq.core.filter.FilterManager; +import com.google.common.collect.Lists; import com.google.common.primitives.Longs; import java.util.HashMap; @@ -98,10 +99,26 @@ public class FeeReceiverSelectorTest { } private static Filter filterWithReceivers(List btcFeeReceiverAddresses) { - return new Filter(null, null, null, null, - null, null, null, null, - false, null, false, null, - null, null, null, null, - btcFeeReceiverAddresses); + return new Filter(Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + false, + Lists.newArrayList(), + false, + null, + null, + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + btcFeeReceiverAddresses, + null, + 0, + null, + null); } } From b0eea78a031a6f5f2629e0d22f83f702510cf2dc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 13:14:18 -0500 Subject: [PATCH 27/52] Remove unused properties (was added again from a merge failure) --- p2p/src/main/java/bisq/network/p2p/P2PService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 571ddd666e..3b0c188e27 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -121,9 +121,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); - private volatile boolean shutDownInProgress; - @Getter - private boolean shutDownComplete; private final Subscription networkReadySubscription; private boolean isBootstrapped; private final KeepAliveManager keepAliveManager; From 01482fb8e623dbf92e54cc878a5e275b23a70914 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 13:21:19 -0500 Subject: [PATCH 28/52] Refactoring Separate validation from setting key --- .../src/main/java/bisq/core/filter/FilterManager.java | 10 +++++++++- .../desktop/main/overlays/windows/FilterWindow.java | 11 +++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 7b54dc0566..8ddaf91d79 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -211,13 +211,17 @@ public class FilterManager { public boolean isValidDevPrivilegeKey(String privKeyString) { try { - filterSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + ECKey filterSigningKey = toECKey(privKeyString); return pubKeyAsHex.equals(Utils.HEX.encode(filterSigningKey.getPubKey())); } catch (Throwable t) { return false; } } + public void setFilterSigningKey(String privKeyString) { + this.filterSigningKey = toECKey(privKeyString); + } + public void publishFilter(Filter filterWithoutSig) { String signatureAsBase64 = getSignature(filterWithoutSig); Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); @@ -443,6 +447,10 @@ public class FilterManager { } } + private ECKey toECKey(String privKeyString) { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + } + private Sha256Hash getSha256Hash(Filter filter) { byte[] filterData = filter.toProtoMessage().toByteArray(); return Sha256Hash.of(filterData); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 8baea00fda..0839cbfb31 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -165,8 +165,9 @@ public class FilterWindow extends Overlay { removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); + String privKeyString = keyInputTextField.getText(); sendButton.setOnAction(e -> { - if (keyInputTextField.getText().isEmpty()) { + if (privKeyString.isEmpty()) { return; } Filter newFilter = new Filter( @@ -189,7 +190,8 @@ public class FilterWindow extends Overlay { readAsList(btcFeeReceiverAddressesInputTextField), filterManager.getOwnerPubKey() ); - if (filterManager.isValidDevPrivilegeKey(keyInputTextField.getText())) { + if (filterManager.isValidDevPrivilegeKey(privKeyString)) { + filterManager.setFilterSigningKey(privKeyString); filterManager.publishFilter(newFilter); removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); hide(); @@ -199,11 +201,12 @@ public class FilterWindow extends Overlay { }); removeFilterMessageButton.setOnAction(e -> { - if (keyInputTextField.getText().isEmpty()) { + if (privKeyString.isEmpty()) { return; } - if (filterManager.isValidDevPrivilegeKey(keyInputTextField.getText())) { + if (filterManager.isValidDevPrivilegeKey(privKeyString)) { + filterManager.setFilterSigningKey(privKeyString); filterManager.removeFilter(); hide(); } else { From bf2ca1fc0c609ff784fa3f5f62a7648dd0c2a625 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 14:07:59 -0500 Subject: [PATCH 29/52] Add BTC prefix or postfix to Price.toFriendlyString method --- .../main/java/bisq/core/monetary/Price.java | 4 +++- .../java/bisq/core/monetary/PriceTest.java | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/bisq/core/monetary/Price.java b/core/src/main/java/bisq/core/monetary/Price.java index 6e58835368..e07a896efe 100644 --- a/core/src/main/java/bisq/core/monetary/Price.java +++ b/core/src/main/java/bisq/core/monetary/Price.java @@ -129,7 +129,9 @@ public class Price extends MonetaryWrapper implements Comparable { } public String toFriendlyString() { - return monetary instanceof Altcoin ? ((Altcoin) monetary).toFriendlyString() : ((Fiat) monetary).toFriendlyString(); + return monetary instanceof Altcoin ? + ((Altcoin) monetary).toFriendlyString() + "/BTC" : + ((Fiat) monetary).toFriendlyString().replace(((Fiat) monetary).currencyCode, "") + "BTC/" + ((Fiat) monetary).currencyCode; } public String toPlainString() { diff --git a/core/src/test/java/bisq/core/monetary/PriceTest.java b/core/src/test/java/bisq/core/monetary/PriceTest.java index 3f82492158..f72aca7ae8 100644 --- a/core/src/test/java/bisq/core/monetary/PriceTest.java +++ b/core/src/test/java/bisq/core/monetary/PriceTest.java @@ -27,14 +27,14 @@ public class PriceTest { Price result = Price.parse("USD", "0.1"); Assert.assertEquals( "Fiat value should be formatted with two decimals.", - "0.10 USD", + "0.10 BTC/USD", result.toFriendlyString() ); result = Price.parse("EUR", "0.1234"); Assert.assertEquals( "Fiat value should be given two decimals", - "0.1234 EUR", + "0.1234 BTC/EUR", result.toFriendlyString() ); @@ -57,19 +57,19 @@ public class PriceTest { Assert.assertEquals( "Comma (',') as decimal separator should be converted to period ('.')", - "0.0001 USD", + "0.0001 BTC/USD", Price.parse("USD", "0,0001").toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded up properly.", - "10000.2346 LTC", + "10000.2346 LTC/BTC", Price.parse("LTC", "10000,23456789").toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded down properly.", - "10000.2345 LTC", + "10000.2345 LTC/BTC", Price.parse("LTC", "10000,23454999").toFriendlyString() ); @@ -95,14 +95,14 @@ public class PriceTest { Price result = Price.valueOf("USD", 1); Assert.assertEquals( "Fiat value should have four decimals.", - "0.0001 USD", + "0.0001 BTC/USD", result.toFriendlyString() ); result = Price.valueOf("EUR", 1234); Assert.assertEquals( "Fiat value should be given two decimals", - "0.1234 EUR", + "0.1234 BTC/EUR", result.toFriendlyString() ); @@ -114,13 +114,13 @@ public class PriceTest { Assert.assertEquals( "Too many decimals should get rounded up properly.", - "10000.2346 LTC", + "10000.2346 LTC/BTC", Price.valueOf("LTC", 1000023456789L).toFriendlyString() ); Assert.assertEquals( "Too many decimals should get rounded down properly.", - "10000.2345 LTC", + "10000.2345 LTC/BTC", Price.valueOf("LTC", 1000023454999L).toFriendlyString() ); From 08632e13ee9055111c63d394dacab9bfb0b5bcbd Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 18:00:36 -0500 Subject: [PATCH 30/52] Add message after dispute opening to show if trade might be an option trade --- .../bisq/core/support/SupportManager.java | 4 + .../core/support/dispute/DisputeManager.java | 300 ++++++++++++------ .../arbitration/ArbitrationManager.java | 6 +- .../dispute/mediation/MediationManager.java | 6 +- .../support/dispute/refund/RefundManager.java | 6 +- 5 files changed, 224 insertions(+), 98 deletions(-) diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index 908a594aee..5d3f33200c 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -62,10 +62,14 @@ public abstract class SupportManager { // We get first the message handler called then the onBootstrapped p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); }); p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + // As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was + // already stored decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); tryApplyMessages(); }); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 4e13adb1fa..2803097d40 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -19,9 +19,16 @@ package bisq.core.support.dispute; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportManager; import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.OpenNewDisputeMessage; @@ -37,13 +44,18 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.SendMailboxMessageListener; +import bisq.common.UserThread; import bisq.common.app.Version; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; import bisq.common.storage.Storage; +import bisq.common.util.MathUtils; import bisq.common.util.Tuple2; +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + import javafx.beans.property.IntegerProperty; import javafx.collections.ObservableList; @@ -51,6 +63,7 @@ import javafx.collections.ObservableList; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -68,6 +81,7 @@ public abstract class DisputeManager disputeListService; + private final PriceFeedService priceFeedService; /////////////////////////////////////////////////////////////////////////////////////////// @@ -82,7 +96,8 @@ public abstract class DisputeManager disputeListService) { + DisputeListService disputeListService, + PriceFeedService priceFeedService) { super(p2PService, walletsSetup); this.tradeWalletService = tradeWalletService; @@ -92,6 +107,7 @@ public abstract class DisputeManager storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent()) { disputeList.add(dispute); - errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing); + sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); } else { // valid case if both have opened a dispute and agent was not online. log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", @@ -286,7 +303,7 @@ public abstract class DisputeManager messages = dispute.getChatMessages(); if (!messages.isEmpty()) { ChatMessage chatMessage = messages.get(0); - PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); + PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); sendAckMessage(chatMessage, sendersPubKeyRing, errorMessage == null, errorMessage); } @@ -468,14 +485,27 @@ public abstract class DisputeManager doSendPeerOpenedDisputeMessage(disputeFromOpener, + contractFromOpener, + pubKeyRing), + 100, TimeUnit.MILLISECONDS); + } + + private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); - return null; + return; } Dispute dispute = new Dispute(disputeListService.getStorage(), @@ -500,91 +530,94 @@ public abstract class DisputeManager storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { - String disputeInfo = getDisputeInfo(dispute); - String disputeMessage = getDisputeIntroForPeer(disputeInfo); - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) - : disputeMessage; - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - pubKeyRing.hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress()); - chatMessage.setSystemMessage(true); - dispute.addAndPersistChatMessage(chatMessage); - disputeList.add(dispute); - // we mirrored dispute already! - Contract contract = dispute.getContract(); - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); - NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); - PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType()); - - log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - peersPubKeyRing, - peerOpenedDisputeMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - disputeList.persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - disputeList.persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - chatMessage.getUid(), errorMessage); - - // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - disputeList.persist(); - } - } - ); - return null; - } else { - // valid case if both have opened a dispute and agent was not online. - log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + // Valid case if both have opened a dispute and agent was not online. + if (storedDisputeOptional.isPresent()) { + log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId()); - return null; + return; } + + String disputeInfo = getDisputeInfo(dispute); + String disputeMessage = getDisputeIntroForPeer(disputeInfo); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION) + : disputeMessage; + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + + addPriceInfoMessage(dispute); + + disputeList.add(dispute); + + // We mirrored dispute already! + Contract contract = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); + PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + + log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + peerOpenedDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + disputeList.persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + disputeList.persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the peerOpenedDisputeMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + disputeList.persist(); + } + } + ); } // dispute agent send result to trader @@ -731,4 +764,87 @@ public abstract class DisputeManager e.getTradeId().equals(tradeId)) .findAny(); } + + // If price was going down between take offer time and open dispute time the buyer has an incentive to + // not send the payment but to try to make a new trade with the better price. We risks to lose part of the + // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated + // proposal). But if gain is larger than this loss he has economically an incentive to default in the trade. + // We do all those calculations to give a hint to mediators to detect option trades. + private void addPriceInfoMessage(Dispute dispute) { + Contract contract = dispute.getContract(); + OfferPayload offerPayload = contract.getOfferPayload(); + Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); + if (priceAtDisputeOpening == null) { + log.info("PriceAtDisputeOpening is null. Price provider might not support that {}.", offerPayload.getCurrencyCode()); + return; + } + + // The amount we would get if we do a new trade with current price + Coin potentialAmountAtDisputeOpening = priceAtDisputeOpening.getAmountByVolume(contract.getTradeVolume()); + Coin buyerSecurityDeposit = Coin.valueOf(offerPayload.getBuyerSecurityDeposit()); + Coin minRefundAtMediatedDispute = Restrictions.getMinRefundAtMediatedDispute(); + // minRefundAtMediatedDispute is always larger as buyerSecurityDeposit at mediated payout, we ignore refund agent case here as there it can be 0. + Coin maxLossSecDeposit = buyerSecurityDeposit.subtract(minRefundAtMediatedDispute); + Coin tradeAmount = contract.getTradeAmount(); + Coin potentialGain = potentialAmountAtDisputeOpening.subtract(tradeAmount).subtract(maxLossSecDeposit); + String optionTradeDetails; + // We don't translate those strings (yet) as it is only displayed to mediators/arbitrators. + String headline; + if (potentialGain.isPositive()) { + headline = "Warning: This might be a potential option trade!"; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible gain from an option trade is: " + potentialGain.toFriendlyString(); + } else { + headline = "It does not appear to be an option trade."; + optionTradeDetails = "\nBTC amount calculated with price at dispute opening: " + potentialAmountAtDisputeOpening.toFriendlyString() + + "\nMax loss of security deposit is: " + maxLossSecDeposit.toFriendlyString() + + "\nPossible loss from an option trade is: " + potentialGain.multiply(-1).toFriendlyString(); + } + + String percentagePriceDetails = offerPayload.isUseMarketBasedPrice() ? + " (market based price was used: " + offerPayload.getMarketPriceMargin() * 100 + "%)" : + " (fix price was used)"; + + String priceInfoText = headline + + "\n\nTrade price: " + contract.getTradePrice().toFriendlyString() + percentagePriceDetails + + "\nTrade amount: " + tradeAmount.toFriendlyString() + + "\nPrice at dispute opening: " + priceAtDisputeOpening.toFriendlyString() + + optionTradeDetails + + "\n\nPlease note: This message is displayed only to the mediator/arbitrator not to the trader. " + + "Only the BTC buyer can do an option trade. The calculation is based on the price at dispute opening. " + + "If seller opens the dispute this might not reflect the buyers point of view. Use this information only " + + "as help for detecting an option trade, it is not a proof for it."; + + // We use the existing msg to copy over the users data + ChatMessage firstMessage = dispute.getChatMessages().get(0); + ChatMessage priceInfoMessage = new ChatMessage(firstMessage.getSupportType(), + firstMessage.getTradeId(), firstMessage.getTraderId(), + firstMessage.isSenderIsTrader(), + priceInfoText, + firstMessage.getSenderNodeAddress()); + priceInfoMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(priceInfoMessage); + } + + @Nullable + private Price getPrice(String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double marketPriceAsDouble = marketPrice.getPrice(); + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e.toString()); + return null; + } + } else { + return null; + } + } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index e063e1c852..f47c3b0c2c 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -28,6 +28,7 @@ import bisq.core.btc.wallet.WalletService; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; @@ -88,9 +89,10 @@ public final class ArbitrationManager extends DisputeManager ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, PubKeyRing pubKeyRing, - MediationDisputeListService mediationDisputeListService) { + MediationDisputeListService mediationDisputeListService, + PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, mediationDisputeListService); + openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 139832a1c1..eb1450b437 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -23,6 +23,7 @@ import bisq.core.btc.wallet.TradeWalletService; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeManager; @@ -74,9 +75,10 @@ public final class RefundManager extends DisputeManager { ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, PubKeyRing pubKeyRing, - RefundDisputeListService refundDisputeListService) { + RefundDisputeListService refundDisputeListService, + PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, refundDisputeListService); + openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService); } /////////////////////////////////////////////////////////////////////////////////////////// From beeac10a99718dec28260614eb9192d17040c0bc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 19:29:08 -0500 Subject: [PATCH 31/52] Change GMT0 to UTC and add UTC time to log at shut down --- .../java/bisq/core/app/misc/ExecutableForAppWithP2p.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java index d4c1420670..b13c5e9e6d 100644 --- a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -159,11 +159,13 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements UserThread.runAfter(() -> { // We check every hour if we are in the target hour. UserThread.runPeriodically(() -> { - int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT0")).getHour(); + int currentHour = ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).getHour(); if (currentHour == target) { log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + - "Shut down node at hour {}" + - "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", target); + "Shut down node at hour {} (UTC time is {})" + + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n", + target, + ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("UTC")).toString()); shutDown(gracefulShutDownHandler); } }, TimeUnit.MINUTES.toSeconds(10)); From f2e8ce582e42c8159137f38b3d4ef30edff0c2ab Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 19:29:59 -0500 Subject: [PATCH 32/52] Remove timeStampMap as it is never read Add hasPrices method Remove final keywords --- .../bisq/core/provider/price/PriceFeedService.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java index 4d2928d5b8..75128ea59b 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java @@ -84,7 +84,6 @@ public class PriceFeedService { private final StringProperty currencyCodeProperty = new SimpleStringProperty(); private final IntegerProperty updateCounter = new SimpleIntegerProperty(0); private long epochInMillisAtLastRequest; - private Map timeStampMap = new HashMap<>(); private long retryDelay = 1; private long requestTs; @Nullable @@ -126,6 +125,10 @@ public class PriceFeedService { request(false); } + public boolean hasPrices() { + return !cache.isEmpty(); + } + public void requestPriceFeed(Consumer resultHandler, FaultHandler faultHandler) { this.priceConsumer = resultHandler; this.faultHandler = faultHandler; @@ -156,7 +159,7 @@ public class PriceFeedService { // At applyPriceToConsumer we also check if price is not exceeding max. age for price data. boolean success = applyPriceToConsumer(); if (success) { - final MarketPrice marketPrice = cache.get(currencyCode); + MarketPrice marketPrice = cache.get(currencyCode); if (marketPrice != null) log.debug("Received new {} from provider {} after {} sec.", marketPrice, @@ -326,7 +329,7 @@ public class PriceFeedService { boolean result = false; String errorMessage = null; if (currencyCode != null) { - final String baseUrl = priceProvider.getBaseUrl(); + String baseUrl = priceProvider.getBaseUrl(); if (cache.containsKey(currencyCode)) { try { MarketPrice marketPrice = cache.get(currencyCode); @@ -383,14 +386,12 @@ public class PriceFeedService { public void onSuccess(@Nullable Tuple2, Map> result) { UserThread.execute(() -> { checkNotNull(result, "Result must not be null at requestAllPrices"); - timeStampMap = result.first; - // Each currency rate has a different timestamp, depending on when // the pricenode aggregate rate was calculated // However, the request timestamp is when the pricenode was queried epochInMillisAtLastRequest = System.currentTimeMillis(); - final Map priceMap = result.second; + Map priceMap = result.second; cache.putAll(priceMap); From bedcd9b10e0b8641369f163927cd39bff6a98cf3 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 29 Aug 2020 19:58:51 -0500 Subject: [PATCH 33/52] Add option trade detection message to result message if case was marked as option trade. Add repeated calls to price provider it no data available (can happen at startup). Show reason in dispute summary. --- .../core/support/dispute/DisputeManager.java | 71 +++++++++++-------- .../arbitration/ArbitrationManager.java | 5 ++ .../support/dispute/refund/RefundManager.java | 8 +++ .../resources/i18n/displayStrings.properties | 43 +++++++---- .../windows/DisputeSummaryWindow.java | 32 +++++---- 5 files changed, 104 insertions(+), 55 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 2803097d40..9998e1f2eb 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -276,7 +276,7 @@ public abstract class DisputeManager addPriceInfoMessage(dispute, counter + 1), 10); + } else { + log.warn("Price provider still has no data after 3 repeated requests and 30 seconds delay. We give up."); + } + return; + } + Contract contract = dispute.getContract(); OfferPayload offerPayload = contract.getOfferPayload(); Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); if (priceAtDisputeOpening == null) { - log.info("PriceAtDisputeOpening is null. Price provider might not support that {}.", offerPayload.getCurrencyCode()); + log.info("Price provider did not provide a price for {}. " + + "This is expected if this currency is not supported by the price providers.", + offerPayload.getCurrencyCode()); return; } @@ -791,7 +807,7 @@ public abstract class DisputeManager { return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); } + @Override + protected void addPriceInfoMessage(Dispute dispute, int counter) { + // At refund agent we do not add the option trade price check as the time for dispute opening is not correct. + // In case of an option trade the mediator adds to the result summary message automatically the system message + // with the option trade detection info so the refund agent can see that as well. + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Message handler /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index dcdf17d0d9..bc299bf7b7 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1039,7 +1039,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -2374,18 +2374,32 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.reason=Reason of dispute -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=Usability -disputeSummaryWindow.reason.protocolViolation=Protocol violation -disputeSummaryWindow.reason.noReply=No reply -disputeSummaryWindow.reason.scam=Scam -disputeSummaryWindow.reason.other=Other -disputeSummaryWindow.reason.bank=Bank -disputeSummaryWindow.reason.optionTrade=Option trade -disputeSummaryWindow.reason.sellerNotResponding=Seller not responding -disputeSummaryWindow.reason.wrongSenderAccount=Wrong sender account -disputeSummaryWindow.reason.peerWasLate=Peer was late -disputeSummaryWindow.reason.tradeAlreadySettled=Trade already settled + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usability +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protocol violation +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=No reply +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Other +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Bank +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes @@ -2394,7 +2408,8 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\ Summary:\n\ Payout amount for BTC buyer: {1}\n\ Payout amount for BTC seller: {2}\n\n\ -Summary notes:\n{3} +Reason for dispute: {3}\n\n\ +Summary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\ Open trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\n\ diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index b55aa07e08..6202b6b204 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -486,18 +486,18 @@ public class DisputeSummaryWindow extends Overlay { } private void addReasonControls() { - reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bug")); - reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.usability")); - reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.protocolViolation")); - reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.noReply")); - reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.scam")); - reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bank")); - reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.other")); - reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.optionTrade")); - reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.sellerNotResponding")); - reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.wrongSenderAccount")); - reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.peerWasLate")); - reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.tradeAlreadySettled")); + reasonWasBugRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BUG.name())); + reasonWasUsabilityIssueRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.USABILITY.name())); + reasonProtocolViolationRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PROTOCOL_VIOLATION.name())); + reasonNoReplyRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.NO_REPLY.name())); + reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SCAM.name())); + reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.BANK_PROBLEMS.name())); + reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OTHER.name())); + reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.OPTION_TRADE.name())); + reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.SELLER_NOT_RESPONDING.name())); + reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.WRONG_SENDER_ACCOUNT.name())); + reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.PEER_WAS_LATE.name())); + reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason." + DisputeResult.Reason.TRADE_ALREADY_SETTLED.name())); HBox feeRadioButtonPane = new HBox(); feeRadioButtonPane.setSpacing(20); @@ -745,12 +745,20 @@ public class DisputeSummaryWindow extends Overlay { disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); dispute.setIsClosed(true); + DisputeResult.Reason reason = disputeResult.getReason(); String text = Res.get("disputeSummaryWindow.close.msg", DisplayUtils.formatDateTime(disputeResult.getCloseDate()), formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + Res.get("disputeSummaryWindow.reason." + reason.name()), disputeResult.summaryNotesProperty().get()); + if (reason == DisputeResult.Reason.OPTION_TRADE && + dispute.getChatMessages().size() > 1 && + dispute.getChatMessages().get(1).isSystemMessage()) { + text += "\n\n" + dispute.getChatMessages().get(1).getMessage(); + } + if (dispute.getSupportType() == SupportType.MEDIATION) { text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); } else if (dispute.getSupportType() == SupportType.REFUND) { From 8d13ff8856ef4544b52ee4762739f217db972283 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 30 Aug 2020 10:02:19 -0500 Subject: [PATCH 34/52] Add comment about size --- core/src/main/java/bisq/core/offer/OfferPayload.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index 3f4411d87a..7b82506690 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -48,6 +48,9 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +// OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a +// backward compatible way. Maybe a candidate when segwit activation is done as hardfork? + @EqualsAndHashCode @Getter @Slf4j From 49d212e654c70394820d6dba04671c16b33b87d4 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 30 Aug 2020 10:37:46 -0500 Subject: [PATCH 35/52] Fix tests. I don't know why the tests failed as I just added an overloaded method and it should not have any impact. There is also one exception which makes it even more obscure. I guess its some test framework issue. See comment at the exceptional handling // If we remove the last argument (isNull()) tests fail. No idea why as the broadcast method has an / overloaded method with nullable listener. Seems a testframework issue as it should not matter if the // method with listener is called with null argument or the other method with no listener. We removed the // null value from all other calls but here we can't as it breaks the test. --- .../P2PDataStorageOnMessageHandlerTest.java | 8 ++--- .../bisq/network/p2p/storage/TestState.java | 29 ++++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java index b5fe473732..29d6ada7dd 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java @@ -33,8 +33,6 @@ import org.junit.Before; import org.junit.Test; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -69,7 +67,7 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test @@ -82,7 +80,7 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } @Test @@ -96,6 +94,6 @@ public class P2PDataStorageOnMessageHandlerTest { this.testState.mockedStorage.onMessage(envelope, mockedConnection); verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class)); } } diff --git a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java index 0ede4a03c8..e71246a031 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java @@ -19,7 +19,6 @@ package bisq.network.p2p.storage; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.BroadcastHandler; import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.storage.messages.AddDataMessage; import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; @@ -51,10 +50,10 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.junit.Assert; - import org.mockito.ArgumentCaptor; +import org.junit.Assert; + import static org.mockito.Mockito.*; /** @@ -160,8 +159,7 @@ public class TestState { /** * Common test helpers that verify the correct events were signaled based on the test expectation and before/after states. */ - private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, - int sequenceNumber) { + private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, int sequenceNumber) { final ArgumentCaptor captor = ArgumentCaptor.forClass(SequenceNumberMap.class); verify(this.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong()); @@ -187,10 +185,9 @@ public class TestState { verify(this.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); if (expectedBroadcast) - verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), - nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), nullable(NodeAddress.class)); else - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } void verifyProtectedStorageAdd(SavedTestState beforeState, @@ -219,13 +216,17 @@ public class TestState { if (expectedBroadcast) { final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); + // If we remove the last argument (isNull()) tests fail. No idea why as the broadcast method has an + // overloaded method with nullable listener. Seems a testframework issue as it should not matter if the + // method with listener is called with null argument or the other method with no listener. We removed the + // null value from all other calls but here we can't as it breaks the test. verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull()); BroadcastMessage broadcastMessage = captor.getValue(); Assert.assertTrue(broadcastMessage instanceof AddDataMessage); Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); } else { - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); } if (expectedSequenceNrMapWrite) { @@ -275,7 +276,7 @@ public class TestState { verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); if (!expectedBroadcast) - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); protectedStorageEntries.forEach(protectedStorageEntry -> { @@ -287,9 +288,9 @@ public class TestState { if (expectedBroadcast) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) - verify(this.mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), nullable(NodeAddress.class)); else - verify(this.mockBroadcaster).broadcast(any(RemoveDataMessage.class), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(any(RemoveDataMessage.class), nullable(NodeAddress.class)); } @@ -319,7 +320,7 @@ public class TestState { Assert.assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate); final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); - verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class), isNull()); + verify(this.mockBroadcaster).broadcast(captor.capture(), nullable(NodeAddress.class)); BroadcastMessage broadcastMessage = captor.getValue(); Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage); @@ -336,7 +337,7 @@ public class TestState { Assert.assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); } - verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class), nullable(BroadcastHandler.Listener.class)); + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), nullable(NodeAddress.class)); verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); } } From 6c60e1739d97fd2b917e0feac46e9d76501e7037 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 30 Aug 2020 12:58:31 -0500 Subject: [PATCH 36/52] Add support for user name for Revolut accounts If a user has an existing account with phone number or email as account ID we show a popup at startup where we require that he sets the user name. This popup has no close button so he is forced to enter a value. If there are multiple account multiple popups will be shown. To not break signed accounts we keep accountId as internal id used for signing. Old accounts get a popup to add the new required field userName but accountId is left unchanged. Newly created accounts fill accountId with the value of userName. In the UI we only use userName. Input validation does only check for length (5-100 chars). Not sure what are the requirements at Revolut. Can be changes easily if anyone gets the specs. --- .../java/bisq/core/app/BisqHeadlessApp.java | 1 + .../main/java/bisq/core/app/BisqSetup.java | 6 ++ .../bisq/core/payment/RevolutAccount.java | 12 ++- .../payload/RevolutAccountPayload.java | 58 ++++++++--- core/src/main/java/bisq/core/user/User.java | 12 +++ .../resources/i18n/displayStrings.properties | 11 ++- .../paymentmethods/RevolutForm.java | 91 +++--------------- .../java/bisq/desktop/main/MainViewModel.java | 27 +++++- .../windows/UpdateRevolutAccountWindow.java | 96 +++++++++++++++++++ .../util/validation/RevolutValidator.java | 9 +- proto/src/main/proto/pb.proto | 1 + 11 files changed, 220 insertions(+), 104 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 567550682b..9307e25b92 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -96,6 +96,7 @@ public class BisqHeadlessApp implements HeadlessApp { bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString())); bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); //TODO move to bisqSetup corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 36ae441eb0..a39d7de09e 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -44,6 +44,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts; import bisq.core.notifications.alerts.price.PriceAlert; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.payment.TradeLimits; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; @@ -221,6 +222,9 @@ public class BisqSetup { @Setter @Nullable private Runnable showPopupIfInvalidBtcConfigHandler; + @Setter + @Nullable + private Consumer> revolutAccountsUpdateHandler; @Getter final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); @@ -824,6 +828,8 @@ public class BisqSetup { priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); + user.onAllServicesInitialized(revolutAccountsUpdateHandler); + allBasicServicesInitialized = true; } diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java index 2afd0b6694..07282769f6 100644 --- a/core/src/main/java/bisq/core/payment/RevolutAccount.java +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -36,11 +36,15 @@ public final class RevolutAccount extends PaymentAccount { return new RevolutAccountPayload(paymentMethod.getId(), id); } - public void setAccountId(String accountId) { - ((RevolutAccountPayload) paymentAccountPayload).setAccountId(accountId); + public void setUserName(String userName) { + ((RevolutAccountPayload) paymentAccountPayload).setUserName(userName); } - public String getAccountId() { - return ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); + public String getUserName() { + return ((RevolutAccountPayload) paymentAccountPayload).getUserName(); + } + + public boolean userNameNotSet() { + return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet(); } } diff --git a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java index 24856d1405..097b56f9ea 100644 --- a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -19,26 +19,36 @@ package bisq.core.payment.payload; import bisq.core.locale.Res; +import bisq.common.proto.ProtoUtil; + import com.google.protobuf.Message; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; + @EqualsAndHashCode(callSuper = true) @ToString -@Setter -@Getter @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { - private String accountId = ""; + // Not used anymore from outside. Only used as internal Id to not break existing account witness objects + private String accountId = null; + + // Was added in 1.3.8 + // To not break signed accounts we keep accountId as internal id used for signing. + // Old accounts get a popup to add the new required field userName but accountId is + // left unchanged. Newly created accounts fill accountId with the value of userName. + // In the UI we only use userName. + @Nullable + private String userName = null; public RevolutAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -52,6 +62,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { private RevolutAccountPayload(String paymentMethod, String id, String accountId, + @Nullable String userName, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -60,20 +71,23 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.accountId = accountId; + this.userName = userName; } @Override public Message toProtoMessage() { - return getPaymentAccountPayloadBuilder() - .setRevolutAccountPayload(protobuf.RevolutAccountPayload.newBuilder() - .setAccountId(accountId)) - .build(); + protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder().setAccountId(accountId); + Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName); + return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } + public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload(); return new RevolutAccountPayload(proto.getPaymentMethodId(), proto.getId(), - proto.getRevolutAccountPayload().getAccountId(), + revolutAccountPayload.getAccountId(), + ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -85,7 +99,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId; + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + userName; } @Override @@ -95,6 +109,26 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public byte[] getAgeWitnessInputData() { - return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + // getAgeWitnessInputData is called at new account creation when accountId is null, we use empty string as + // it has been the case before + String input = this.accountId == null ? "" : this.accountId; + return super.getAgeWitnessInputData(input.getBytes(StandardCharsets.UTF_8)); + } + + public void setUserName(@Nullable String userName) { + this.userName = userName; + // We only set accountId to userName for new accounts. Existing accounts have accountId set with email + // or phone nr. and we keep that to not break account signing. + if (accountId == null) { + accountId = userName; + } + } + + public String getUserName() { + return userName != null ? userName : accountId; + } + + public boolean userNameNotSet() { + return userName == null; } } diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 75891395d2..0f6957c83e 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -24,6 +24,7 @@ import bisq.core.locale.TradeCurrency; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.refund.refundagent.RefundAgent; @@ -50,6 +51,7 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import lombok.AllArgsConstructor; @@ -126,6 +128,16 @@ public class User implements PersistedDataHost { // API /////////////////////////////////////////////////////////////////////////////////////////// + public void onAllServicesInitialized(@Nullable Consumer> resultHandler) { + if (resultHandler != null) { + resultHandler.accept(paymentAccountsAsObservable.stream() + .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) + .map(paymentAccount -> (RevolutAccount) paymentAccount) + .filter(RevolutAccount::userNameNotSet) + .collect(Collectors.toList())); + } + } + @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 276eecf83b..f48d4a1a06 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2987,6 +2987,7 @@ seed.restore.error=An error occurred when restoring the wallets with seed words. payment.account=Account payment.account.no=Account no. payment.account.name=Account name +payment.account.userName=User name payment.account.owner=Account owner full name payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region @@ -3021,8 +3022,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Email or phone no. payment.venmo.venmoUserName=Venmo username payment.popmoney.accountId=Email or phone no. -payment.revolut.email=Email -payment.revolut.phoneNr=Registered phone no. payment.promptPay.promptPayId=Citizen ID/Tax ID or phone no. payment.supportedCurrencies=Supported currencies payment.limitations=Limitations @@ -3126,8 +3125,12 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \ For example, Bank of America and Wells Fargo no longer allow such deposits. -payment.revolut.info=Please be sure that the phone number you used for your Revolut account is registered at Revolut \ - otherwise the BTC buyer cannot send you the funds. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\n\ + Your existing Revolut account ({1}) does not has set the ''User name''.\n\ + Please enter your Revolut ''User name'' to update your account data.\n\ + This will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\ \n\ diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java index a89803c835..9ddd087419 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -23,8 +23,6 @@ import bisq.desktop.util.Layout; import bisq.desktop.util.validation.RevolutValidator; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.locale.Country; -import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; @@ -34,46 +32,28 @@ import bisq.core.payment.payload.RevolutAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; -import com.jfoenix.controls.JFXComboBox; - -import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; - -import javafx.collections.FXCollections; - -import javafx.util.StringConverter; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; -import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; private RevolutValidator validator; - private InputTextField accountIdInputTextField; - private Country selectedCountry; + private InputTextField userNameInputTextField; public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - String accountId = ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); - addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, getTitle(accountId), accountId); + String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName); return gridRow; } - private static String getTitle(String accountId) { - // From 0.9.4 on we only allow phone nr. as with emails we got too many disputes as users used an email which was - // not registered at Revolut. It seems that phone numbers need to be registered at least we have no reports from - // arbitrators with such cases. Thought email is still supported for backward compatibility. - // We might still get emails from users who have registered when email was supported - return accountId.contains("@") ? Res.get("payment.revolut.email") : Res.get("payment.revolut.phoneNr"); - } - public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) { @@ -86,63 +66,16 @@ public class RevolutForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - // country selection is added only to prevent anymore email id input and - // solely to validate the given phone number - ComboBox countryComboBox = addCountrySelection(); - setCountryComboBoxAction(countryComboBox); - countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllRevolutCountries())); - - accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.revolut.phoneNr")); - accountIdInputTextField.setValidator(validator); - accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { - account.setAccountId(newValue.trim()); + userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName")); + userNameInputTextField.setValidator(validator); + userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + account.setUserName(newValue.trim()); updateFromInputs(); }); addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); - - //set default country as selected - selectedCountry = CountryUtil.getDefaultCountry(); - if (CountryUtil.getAllRevolutCountries().contains(selectedCountry)) { - countryComboBox.getSelectionModel().select(selectedCountry); - } - } - - ComboBox addCountrySelection() { - HBox hBox = new HBox(); - - hBox.setSpacing(5); - ComboBox countryComboBox = new JFXComboBox<>(); - hBox.getChildren().add(countryComboBox); - - addTopLabelWithVBox(gridPane, ++gridRow, Res.get("payment.bank.country"), hBox, 0); - - countryComboBox.setPromptText(Res.get("payment.select.bank.country")); - countryComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Country country) { - return country.name + " (" + country.code + ")"; - } - - @Override - public Country fromString(String s) { - return null; - } - }); - return countryComboBox; - } - - void setCountryComboBoxAction(ComboBox countryComboBox) { - countryComboBox.setOnAction(e -> { - selectedCountry = countryComboBox.getSelectionModel().getSelectedItem(); - updateFromInputs(); - accountIdInputTextField.resetValidation(); - accountIdInputTextField.validate(); - accountIdInputTextField.requestFocus(); - countryComboBox.requestFocus(); - }); } private void addCurrenciesGrid(boolean isEditable) { @@ -161,18 +94,18 @@ public class RevolutForm extends PaymentMethodForm { @Override protected void autoFillNameTextField() { - setAccountNameWithString(accountIdInputTextField.getText()); + setAccountNameWithString(userNameInputTextField.getText()); } @Override public void addFormForDisplayAccount() { gridRowFrom = gridRow; - addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"), + addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.userName"), account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); - String accountId = account.getAccountId(); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, getTitle(accountId), accountId).second; + String userName = account.getUserName(); + TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); @@ -181,7 +114,7 @@ public class RevolutForm extends PaymentMethodForm { @Override public void updateAllInputsValid() { allInputsValid.set(isAccountNameValid() - && validator.validate(account.getAccountId(), selectedCountry.code).isValid + && validator.validate(account.getUserName()).isValid && account.getTradeCurrencies().size() > 0); } } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index da37762ac3..cdd3cbe2bf 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow; import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow; import bisq.desktop.main.overlays.windows.TacWindow; import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; +import bisq.desktop.main.overlays.windows.UpdateRevolutAccountWindow; import bisq.desktop.main.overlays.windows.WalletPasswordWindow; import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; import bisq.desktop.main.presentation.AccountPresentation; @@ -49,6 +50,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.AliPayAccount; import bisq.core.payment.CryptoCurrencyAccount; +import bisq.core.payment.RevolutAccount; import bisq.core.presentation.BalancePresentation; import bisq.core.presentation.SupportTicketsPresentation; import bisq.core.presentation.TradePresentation; @@ -87,12 +89,15 @@ import javafx.beans.property.StringProperty; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; +import java.util.List; import java.util.Optional; import java.util.PriorityQueue; import java.util.Queue; import java.util.Random; +import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -300,10 +305,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { .useReportBugButton() .show())); bisqSetup.setDisplayTorNetworkSettingsHandler(show -> { - if (show) + if (show) { torNetworkSettingsWindow.show(); - else + } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); + } }); bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) .actionButtonText(Res.get("settings.net.reSyncSPVChainButton")) @@ -374,6 +380,12 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { + // We copy the array as we will mutate it later + showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); + }); + + corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> new Popup() .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) .useShutDownButton() @@ -403,6 +415,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); } + private void showRevolutAccountUpdateWindow(List revolutAccountList) { + if (!revolutAccountList.isEmpty()) { + RevolutAccount revolutAccount = revolutAccountList.get(0); + revolutAccountList.remove(0); + new UpdateRevolutAccountWindow(revolutAccount, user).onClose(() -> { + // We delay a bit in case we have multiple account for better UX + UserThread.runAfter(() -> showRevolutAccountUpdateWindow(revolutAccountList), 300, TimeUnit.MILLISECONDS); + }).show(); + } + } + private void setupP2PNumPeersWatcher() { p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { int numPeers = (int) newValue; diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java new file mode 100644 index 0000000000..0c72c5a1a0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UpdateRevolutAccountWindow.java @@ -0,0 +1,96 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.RevolutValidator; + +import bisq.core.locale.Res; +import bisq.core.payment.RevolutAccount; +import bisq.core.user.User; + +import javafx.scene.Scene; + +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addLabel; + +public class UpdateRevolutAccountWindow extends Overlay { + private final RevolutValidator revolutValidator; + private final RevolutAccount revolutAccount; + private final User user; + private InputTextField userNameInputTextField; + + public UpdateRevolutAccountWindow(RevolutAccount revolutAccount, User user) { + super(); + this.revolutAccount = revolutAccount; + this.user = user; + type = Type.Attention; + hideCloseButton = true; + revolutValidator = new RevolutValidator(); + actionButtonText = Res.get("shared.save"); + } + + @Override + protected void setupKeyHandler(Scene scene) { + // We do not support enter or escape here + } + + @Override + public void show() { + if (headLine == null) + headLine = Res.get("payment.revolut.addUserNameInfo.headLine"); + + width = 868; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + private void addContent() { + addLabel(gridPane, ++rowIndex, Res.get("payment.account.revolut.addUserNameInfo", Res.get("payment.revolut.info"), revolutAccount.getAccountName())); + userNameInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("payment.account.userName"), Layout.COMPACT_FIRST_ROW_DISTANCE); + userNameInputTextField.setValidator(revolutValidator); + userNameInputTextField.textProperty().addListener((observable, oldValue, newValue) -> + actionButton.setDisable(!revolutValidator.validate(newValue).isValid)); + } + + @Override + protected void addButtons() { + super.addButtons(); + + // We do not allow close in case the userName is not correctly added so we + // overwrote the default handler + actionButton.setOnAction(event -> { + String userName = userNameInputTextField.getText(); + if (revolutValidator.validate(userName).isValid) { + revolutAccount.setUserName(userName); + user.persist(); + closeHandlerOptional.ifPresent(Runnable::run); + hide(); + } + }); + actionButton.setDisable(true); + } + +} + diff --git a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java index 44eea0023d..6005dc681a 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java @@ -17,10 +17,13 @@ package bisq.desktop.util.validation; -public final class RevolutValidator extends PhoneNumberValidator { +public final class RevolutValidator extends LengthValidator { + public RevolutValidator() { + // Not sure what are requirements for Revolut user names + super(5, 100); + } - public ValidationResult validate(String input, String code) { - super.setIsoCountryCode(code); + public ValidationResult validate(String input) { return super.validate(input); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index b581cf4009..bf44de0e7f 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1085,6 +1085,7 @@ message PopmoneyAccountPayload { message RevolutAccountPayload { string account_id = 1; + string user_name = 2; } message PerfectMoneyAccountPayload { From 59ebefc7d9ffa03753f39757ebc666966502c7a6 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 09:47:35 -0500 Subject: [PATCH 37/52] Revert to use empty string as default for accountId instead of null. Prefer to keep old functionality here... --- .../payment/payload/RevolutAccountPayload.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java index 097b56f9ea..7c8926ab10 100644 --- a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -40,7 +40,7 @@ import javax.annotation.Nullable; @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { // Not used anymore from outside. Only used as internal Id to not break existing account witness objects - private String accountId = null; + private String accountId = ""; // Was added in 1.3.8 // To not break signed accounts we keep accountId as internal id used for signing. @@ -76,7 +76,8 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public Message toProtoMessage() { - protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder().setAccountId(accountId); + protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() + .setAccountId(accountId); Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName); return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } @@ -99,7 +100,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + userName; + return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + getUserName(); } @Override @@ -109,17 +110,15 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public byte[] getAgeWitnessInputData() { - // getAgeWitnessInputData is called at new account creation when accountId is null, we use empty string as - // it has been the case before - String input = this.accountId == null ? "" : this.accountId; - return super.getAgeWitnessInputData(input.getBytes(StandardCharsets.UTF_8)); + // getAgeWitnessInputData is called at new account creation when accountId is empty string. + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); } public void setUserName(@Nullable String userName) { this.userName = userName; // We only set accountId to userName for new accounts. Existing accounts have accountId set with email // or phone nr. and we keep that to not break account signing. - if (accountId == null) { + if (accountId.isEmpty()) { accountId = userName; } } From 591011e5169d7f350f09a48bdb91a24f23902cfc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 09:51:53 -0500 Subject: [PATCH 38/52] Add comment --- .../java/bisq/desktop/util/validation/RevolutValidator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java index 6005dc681a..0cf464b30a 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/RevolutValidator.java @@ -20,6 +20,10 @@ package bisq.desktop.util.validation; public final class RevolutValidator extends LengthValidator { public RevolutValidator() { // Not sure what are requirements for Revolut user names + // Please keep in mind that even we force users to set user name at startup we should handle also the case + // that the old accountID as phone number or email is displayed at the username text field and we do not + // want to break validation in those cases. So being too strict on the validators might cause more troubles + // as its worth... super(5, 100); } From bc802c861d707a642ffbad25771651f9619d880c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 10:37:58 -0500 Subject: [PATCH 39/52] Change log level to avoid too verbose logs --- p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java index d1288ffe94..cdfdde5c46 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/Broadcaster.java @@ -98,7 +98,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { broadcastRequests.add(new BroadcastRequest(message, sender, listener)); // Keep that log on INFO for better debugging if the feature works as expected. Later it can // be remove or set to DEBUG - log.info("Broadcast requested for {}. We queue it up for next bundled broadcast.", + log.debug("Broadcast requested for {}. We queue it up for next bundled broadcast.", message.getClass().getSimpleName()); if (timer == null) { @@ -108,7 +108,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler { private void maybeBroadcastBundle() { if (!broadcastRequests.isEmpty()) { - log.info("Broadcast bundled requests of {} messages. Message types: {}", + log.debug("Broadcast bundled requests of {} messages. Message types: {}", broadcastRequests.size(), broadcastRequests.stream().map(e -> e.getMessage().getClass().getSimpleName()).collect(Collectors.toList())); BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager, this); From f7951d5943d1f33ed1052eedb2a4f9f0234cc9d7 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 10:38:46 -0500 Subject: [PATCH 40/52] Combine if/else branches. Improve comments and variables --- .../bisq/network/p2p/peers/BroadcastHandler.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java index cc5b45bc52..d370be6bd3 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/BroadcastHandler.java @@ -289,18 +289,14 @@ public class BroadcastHandler implements PeerManager.Listener { .map(Broadcaster.BroadcastRequest::getListener) .forEach(listener -> listener.onSufficientlyBroadcast(broadcastRequests)); } else { - // Number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. + // We check if number of open requests to peers is less than we need to reach numOfCompletedBroadcastsTarget. // Thus we never can reach required resilience as too many numOfFailedBroadcasts occurred. - int openRequests = numPeersForBroadcast - numOfCompletedBroadcasts - numOfFailedBroadcasts; - int maxPossibleSuccessCases = openRequests + numOfCompletedBroadcasts; + int maxPossibleSuccessCases = numPeersForBroadcast - numOfFailedBroadcasts; // We subtract 1 as we want to have it called only once, with a < comparision we would trigger repeatedly. - if (maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1) { - broadcastRequests.stream() - .filter(broadcastRequest -> broadcastRequest.getListener() != null) - .map(Broadcaster.BroadcastRequest::getListener) - .forEach(listener -> listener.onNotSufficientlyBroadcast(numOfCompletedBroadcasts, numOfFailedBroadcasts)); - } else if (timeoutTriggered && numOfCompletedBroadcasts < numOfCompletedBroadcastsTarget) { - // We did not reach resilience level and timeout prevents to reach it later + boolean notEnoughSucceededOrOpen = maxPossibleSuccessCases == numOfCompletedBroadcastsTarget - 1; + // We did not reach resilience level and timeout prevents to reach it later + boolean timeoutAndNotEnoughSucceeded = timeoutTriggered && numOfCompletedBroadcasts < numOfCompletedBroadcastsTarget; + if (notEnoughSucceededOrOpen || timeoutAndNotEnoughSucceeded) { broadcastRequests.stream() .filter(broadcastRequest -> broadcastRequest.getListener() != null) .map(Broadcaster.BroadcastRequest::getListener) From e2de5221bff01c8c269259b8a68c6c34eb84cdff Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 10:51:21 -0500 Subject: [PATCH 41/52] Fix + prefix in string keys --- core/src/main/resources/i18n/displayStrings.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 276eecf83b..3702495490 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1169,8 +1169,8 @@ setting.about.support=Support Bisq setting.about.def=Bisq is not a company—it is a project open to the community. If you want to participate or support Bisq please follow the links below. setting.about.contribute=Contribute setting.about.providers=Data providers -+setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. -+setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Market prices provided by setting.about.feeEstimation.label=Mining fee estimation provided by setting.about.versionDetails=Version details From cc20a3b6c7c325c7670fd9bb4582b5e887a583cc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 12:33:45 -0500 Subject: [PATCH 42/52] - Add support for multiple pubKeys - Add more validation - Add SignerPubKeyAsHex to filter - Refactor add/remove filter code --- .../main/java/bisq/core/filter/Filter.java | 26 +++- .../java/bisq/core/filter/FilterManager.java | 124 ++++++++++++++---- .../core/user/UserPayloadModelVOTest.java | 1 + .../core/util/FeeReceiverSelectorTest.java | 1 + .../main/overlays/windows/FilterWindow.java | 77 +++++------ proto/src/main/proto/pb.proto | 1 + 6 files changed, 159 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 93744ef9e9..f6c28bc575 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -60,7 +60,10 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { // created by cloning the object with a non-null sig. @Nullable private final String signatureAsBase64; - // The pub key used for the data protection in teh p2p storage + // The pub EC key from the dev who has signed and published the filter (different to ownerPubKeyBytes) + private final String signerPubKeyAsHex; + + // The pub key used for the data protection in the p2p storage private final byte[] ownerPubKeyBytes; private final boolean disableDao; private final String disableDaoBelowVersion; @@ -104,7 +107,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getOwnerPubKeyBytes(), filter.getCreationDate(), filter.getExtraDataMap(), - signatureAsBase64); + signatureAsBase64, + filter.getSignerPubKeyAsHex()); } // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again @@ -129,7 +133,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getOwnerPubKeyBytes(), filter.getCreationDate(), filter.getExtraDataMap(), - null); + null, + filter.getSignerPubKeyAsHex()); } public Filter(List bannedOfferIds, @@ -149,7 +154,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { List refundAgents, List bannedSignerPubKeys, List btcFeeReceiverAddresses, - PublicKey ownerPubKey) { + PublicKey ownerPubKey, + String signerPubKeyAsHex) { this(bannedOfferIds, bannedNodeAddress, bannedPaymentAccounts, @@ -170,7 +176,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { Sig.getPublicKeyBytes(ownerPubKey), System.currentTimeMillis(), null, - null); + null, + signerPubKeyAsHex); } @@ -199,7 +206,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { byte[] ownerPubKeyBytes, long creationDate, @Nullable Map extraDataMap, - @Nullable String signatureAsBase64) { + @Nullable String signatureAsBase64, + String signerPubKeyAsHex) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -221,6 +229,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.creationDate = creationDate; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.signatureAsBase64 = signatureAsBase64; + this.signerPubKeyAsHex = signerPubKeyAsHex; // ownerPubKeyBytes can be null when called from tests if (ownerPubKeyBytes != null) { @@ -254,6 +263,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { .addAllBannedSignerPubKeys(bannedSignerPubKeys) .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) + .setSignerPubKeyAsHex(signerPubKeyAsHex) .setCreationDate(creationDate); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); @@ -288,7 +298,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { proto.getOwnerPubKeyBytes().toByteArray(), proto.getCreationDate(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), - proto.getSignatureAsBase64() + proto.getSignatureAsBase64(), + proto.getSignerPubKeyAsHex() ); } @@ -316,6 +327,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork + ",\n btcNodes=" + btcNodes + ",\n signatureAsBase64='" + signatureAsBase64 + '\'' + + ",\n signerPubKeyAsHex='" + signerPubKeyAsHex + '\'' + ",\n ownerPubKeyBytes=" + Utilities.bytesAsHexString(ownerPubKeyBytes) + ",\n disableDao=" + disableDao + ",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' + diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 8ddaf91d79..816c779605 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -38,7 +38,6 @@ import bisq.common.crypto.KeyRing; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.Utils; import javax.inject.Inject; import javax.inject.Named; @@ -55,6 +54,7 @@ import java.nio.charset.StandardCharsets; import java.math.BigInteger; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -94,8 +94,7 @@ public class FilterManager { private final boolean ignoreDevMsg; private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); private final List listeners = new CopyOnWriteArrayList<>(); - private final String pubKeyAsHex; - + private final List publicKeys; private ECKey filterSigningKey; @@ -120,9 +119,12 @@ public class FilterManager { this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; - pubKeyAsHex = useDevPrivilegeKeys ? - DevEnv.DEV_PRIVILEGE_PUB_KEY : - "022ac7b7766b0aedff82962522c2c14fb8d1961dabef6e5cfd10edc679456a32f1"; + //todo test + publicKeys = !useDevPrivilegeKeys ? + Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : + List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", + "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f"); + } @@ -139,6 +141,7 @@ public class FilterManager { .map(ProtectedStorageEntry::getProtectedStoragePayload) .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) .map(protectedStoragePayload -> (Filter) protectedStoragePayload) + .filter(this::isFilterPublicKeyInList) .filter(this::verifySignature) .forEach(this::onFilterAddedFromNetwork); @@ -149,7 +152,7 @@ public class FilterManager { .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (verifySignature(filter)) { + if (isFilterPublicKeyInList(filter) && verifySignature(filter)) { onFilterAddedFromNetwork(filter); } }); @@ -161,7 +164,7 @@ public class FilterManager { .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (verifySignature(filter)) { + if (isFilterPublicKeyInList(filter) && verifySignature(filter)) { onFilterRemovedFromNetwork(filter); } }); @@ -209,20 +212,24 @@ public class FilterManager { }); } - public boolean isValidDevPrivilegeKey(String privKeyString) { - try { - ECKey filterSigningKey = toECKey(privKeyString); - return pubKeyAsHex.equals(Utils.HEX.encode(filterSigningKey.getPubKey())); - } catch (Throwable t) { + public boolean canAddDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { return false; } + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("There is no persisted dev filter to be removed."); + return false; + } + return true; } - public void setFilterSigningKey(String privKeyString) { - this.filterSigningKey = toECKey(privKeyString); + public String getSignerPubKeyAsHex(String privKeyString) { + ECKey ecKey = toECKey(privKeyString); + return getPubKeyAsHex(ecKey); } - public void publishFilter(Filter filterWithoutSig) { + public void addDevFilter(Filter filterWithoutSig, String privKeyString) { + setFilterSigningKey(privKeyString); String signatureAsBase64 = getSignature(filterWithoutSig); Filter filterWithSig = Filter.cloneWithSig(filterWithoutSig, signatureAsBase64); user.setDevelopersFilter(filterWithSig); @@ -230,8 +237,42 @@ public class FilterManager { p2PService.addProtectedStorageEntry(filterWithSig); } + public boolean canRemoveDevFilter(String privKeyString) { + if (privKeyString == null || privKeyString.isEmpty()) { + return false; + } - public void removeFilter() { + Filter developersFilter = getDevFilter(); + if (developersFilter == null) { + log.warn("There is no persisted dev filter to be removed."); + return false; + } + + if (!isPublicKeyInList(developersFilter.getSignerPubKeyAsHex())) { + log.warn("The SignerPubKey from the filter is not in our pubKey list. " + + "filterSignerPubKey={}, publicKeys={}", developersFilter.getSignerPubKeyAsHex(), publicKeys); + return false; + } + + if (!isValidDevPrivilegeKey(privKeyString)) { + log.warn("There is no persisted dev filter to be removed."); + return false; + } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (!developersFilter.getSignerPubKeyAsHex().equals(pubKeyAsHex)) { + log.warn("pubKeyAsHex derived from private key does not match filterSignerPubKey. " + + "filterSignerPubKey={}, pubKeyAsHex derived from private key={}", + developersFilter.getSignerPubKeyAsHex(), pubKeyAsHex); + return false; + } + + return true; + } + + public void removeDevFilter(String privKeyString) { + setFilterSigningKey(privKeyString); Filter filterWithSig = user.getDevelopersFilter(); if (filterWithSig == null) { // Should not happen as UI button is deactivated in that case @@ -259,10 +300,14 @@ public class FilterManager { } @Nullable - public Filter getDevelopersFilter() { + public Filter getDevFilter() { return user.getDevelopersFilter(); } + public PublicKey getOwnerPubKey() { + return keyRing.getSignatureKeyPair().getPublic(); + } + public boolean isCurrencyBanned(String currencyCode) { return getFilter() != null && getFilter().getBannedCurrencies() != null && @@ -347,10 +392,6 @@ public class FilterManager { .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); } - public PublicKey getOwnerPubKey() { - return keyRing.getSignatureKeyPair().getPublic(); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -423,6 +464,20 @@ public class FilterManager { configFileEditor.clearOption(optionName); } + private boolean isValidDevPrivilegeKey(String privKeyString) { + try { + ECKey filterSigningKey = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(filterSigningKey); + return isPublicKeyInList(pubKeyAsHex); + } catch (Throwable t) { + return false; + } + } + + private void setFilterSigningKey(String privKeyString) { + this.filterSigningKey = toECKey(privKeyString); + } + private String getSignature(Filter filterWithoutSig) { Sha256Hash hash = getSha256Hash(filterWithoutSig); ECKey.ECDSASignature ecdsaSignature = filterSigningKey.sign(hash); @@ -430,6 +485,15 @@ public class FilterManager { return new String(Base64.encode(encodeToDER), StandardCharsets.UTF_8); } + private boolean isFilterPublicKeyInList(Filter filter) { + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + if (!isPublicKeyInList(signerPubKeyAsHex)) { + log.warn("signerPubKeyAsHex from filter is not part of our pub key list. filter={}, publicKeys={}", filter, publicKeys); + return false; + } + return true; + } + private boolean verifySignature(Filter filter) { try { Filter filterForSigVerification = Filter.cloneWithoutSig(filter); @@ -439,7 +503,9 @@ public class FilterManager { byte[] sigData = Base64.decode(filter.getSignatureAsBase64()); ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(sigData); - ECKey ecPubKey = ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)); + String signerPubKeyAsHex = filter.getSignerPubKeyAsHex(); + byte[] decode = HEX.decode(signerPubKeyAsHex); + ECKey ecPubKey = ECKey.fromPublicOnly(decode); return ecPubKey.verify(hash, ecdsaSignature); } catch (Throwable e) { log.warn("verifySignature failed. filter={}", filter); @@ -455,4 +521,16 @@ public class FilterManager { byte[] filterData = filter.toProtoMessage().toByteArray(); return Sha256Hash.of(filterData); } + + private String getPubKeyAsHex(ECKey ecKey) { + return HEX.encode(ecKey.getPubKey()); + } + + private boolean isPublicKeyInList(String pubKeyAsHex) { + boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); + if (!isPublicKeyInList) { + log.warn("pubKeyAsHex is not part of our pub key list. pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); + } + return isPublicKeyInList; + } } diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index de685c9ba4..050fce18ec 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -60,6 +60,7 @@ public class UserPayloadModelVOTest { null, 0, null, + null, null)); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index 2f059edd84..e5319f8ecd 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -119,6 +119,7 @@ public class FeeReceiverSelectorTest { null, 0, null, + null, null); } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 0839cbfb31..8ca9aa4484 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -79,7 +79,7 @@ public class FilterWindow extends Overlay { if (headLine == null) headLine = Res.get("filterWindow.headline"); - width = 968; + width = 1000; createGridPane(); @@ -87,7 +87,7 @@ public class FilterWindow extends Overlay { scrollPane.setContent(gridPane); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); - scrollPane.setMaxHeight(1000); + scrollPane.setMaxHeight(700); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); addHeadLine(); @@ -139,7 +139,7 @@ public class FilterWindow extends Overlay { InputTextField disableDaoBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableDaoBelowVersion")); InputTextField disableTradeBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); - final Filter filter = filterManager.getDevelopersFilter(); + Filter filter = filterManager.getDevFilter(); if (filter != null) { setupFieldFromList(offerIdsInputTextField, filter.getBannedOfferIds()); setupFieldFromList(nodesInputTextField, filter.getBannedNodeAddress()); @@ -162,55 +162,50 @@ public class FilterWindow extends Overlay { } Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); - removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); + removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); - String privKeyString = keyInputTextField.getText(); sendButton.setOnAction(e -> { - if (privKeyString.isEmpty()) { - return; - } - Filter newFilter = new Filter( - readAsList(offerIdsInputTextField), - readAsList(nodesInputTextField), - readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), - readAsList(bannedCurrenciesInputTextField), - readAsList(bannedPaymentMethodsInputTextField), - readAsList(arbitratorsInputTextField), - readAsList(seedNodesInputTextField), - readAsList(priceRelayNodesInputTextField), - preventPublicBtcNetworkCheckBox.isSelected(), - readAsList(btcNodesInputTextField), - disableDaoCheckBox.isSelected(), - disableDaoBelowVersionInputTextField.getText(), - disableTradeBelowVersionInputTextField.getText(), - readAsList(mediatorsInputTextField), - readAsList(refundAgentsInputTextField), - readAsList(bannedSignerPubKeysInputTextField), - readAsList(btcFeeReceiverAddressesInputTextField), - filterManager.getOwnerPubKey() - ); - if (filterManager.isValidDevPrivilegeKey(privKeyString)) { - filterManager.setFilterSigningKey(privKeyString); - filterManager.publishFilter(newFilter); - removeFilterMessageButton.setDisable(filterManager.getDevelopersFilter() == null); + String privKeyString = keyInputTextField.getText(); + if (filterManager.canAddDevFilter(privKeyString)) { + String signerPubKeyAsHex = filterManager.getSignerPubKeyAsHex(privKeyString); + Filter newFilter = new Filter( + readAsList(offerIdsInputTextField), + readAsList(nodesInputTextField), + readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), + readAsList(bannedCurrenciesInputTextField), + readAsList(bannedPaymentMethodsInputTextField), + readAsList(arbitratorsInputTextField), + readAsList(seedNodesInputTextField), + readAsList(priceRelayNodesInputTextField), + preventPublicBtcNetworkCheckBox.isSelected(), + readAsList(btcNodesInputTextField), + disableDaoCheckBox.isSelected(), + disableDaoBelowVersionInputTextField.getText(), + disableTradeBelowVersionInputTextField.getText(), + readAsList(mediatorsInputTextField), + readAsList(refundAgentsInputTextField), + readAsList(bannedSignerPubKeysInputTextField), + readAsList(btcFeeReceiverAddressesInputTextField), + filterManager.getOwnerPubKey(), + signerPubKeyAsHex + ); + + filterManager.addDevFilter(newFilter, privKeyString); + removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null); hide(); } else { - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); } }); removeFilterMessageButton.setOnAction(e -> { - if (privKeyString.isEmpty()) { - return; - } - - if (filterManager.isValidDevPrivilegeKey(privKeyString)) { - filterManager.setFilterSigningKey(privKeyString); - filterManager.removeFilter(); + String privKeyString = keyInputTextField.getText(); + if (filterManager.canRemoveDevFilter(privKeyString)) { + filterManager.removeDevFilter(privKeyString); hide(); } else { - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show(); } }); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index cf448e81a5..c1e8de2f5a 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -639,6 +639,7 @@ message Filter { repeated string bannedSignerPubKeys = 19; repeated string btc_fee_receiver_addresses = 20; int64 creation_date = 21; + string signer_pub_key_as_hex = 22; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older From 1b5e449114dfdc4e073657debfb4fd45adf1985a Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 12:34:16 -0500 Subject: [PATCH 43/52] Remove dev test code --- core/src/main/java/bisq/core/filter/FilterManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 816c779605..0ebd0a88dd 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -119,8 +119,7 @@ public class FilterManager { this.providersRepository = providersRepository; this.ignoreDevMsg = ignoreDevMsg; - //todo test - publicKeys = !useDevPrivilegeKeys ? + publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f"); From 9a142be86e14d76f01e9841f13aeef19efdc1811 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 12:51:01 -0500 Subject: [PATCH 44/52] Add bannedPrivilegedDevPubKeys field --- .../main/java/bisq/core/filter/Filter.java | 27 +++++++++++++------ .../resources/i18n/displayStrings.properties | 3 ++- .../main/overlays/windows/FilterWindow.java | 17 +++++++----- proto/src/main/proto/pb.proto | 1 + 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index f6c28bc575..ff63336364 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -77,6 +77,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final long creationDate; + private final List bannedPrivilegedDevPubKeys; + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new // field in a class would break that hash and therefore break the storage mechanism. @@ -108,7 +110,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getCreationDate(), filter.getExtraDataMap(), signatureAsBase64, - filter.getSignerPubKeyAsHex()); + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys()); } // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again @@ -134,7 +137,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getCreationDate(), filter.getExtraDataMap(), null, - filter.getSignerPubKeyAsHex()); + filter.getSignerPubKeyAsHex(), + filter.getBannedPrivilegedDevPubKeys()); } public Filter(List bannedOfferIds, @@ -155,7 +159,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { List bannedSignerPubKeys, List btcFeeReceiverAddresses, PublicKey ownerPubKey, - String signerPubKeyAsHex) { + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys) { this(bannedOfferIds, bannedNodeAddress, bannedPaymentAccounts, @@ -177,7 +182,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { System.currentTimeMillis(), null, null, - signerPubKeyAsHex); + signerPubKeyAsHex, + bannedPrivilegedDevPubKeys); } @@ -207,7 +213,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { long creationDate, @Nullable Map extraDataMap, @Nullable String signatureAsBase64, - String signerPubKeyAsHex) { + String signerPubKeyAsHex, + List bannedPrivilegedDevPubKeys) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -230,6 +237,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); this.signatureAsBase64 = signatureAsBase64; this.signerPubKeyAsHex = signerPubKeyAsHex; + this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; // ownerPubKeyBytes can be null when called from tests if (ownerPubKeyBytes != null) { @@ -264,7 +272,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .setSignerPubKeyAsHex(signerPubKeyAsHex) - .setCreationDate(creationDate); + .setCreationDate(creationDate) + .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); @@ -299,7 +308,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { proto.getCreationDate(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), proto.getSignatureAsBase64(), - proto.getSignerPubKeyAsHex() + proto.getSignerPubKeyAsHex(), + ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()) ); } @@ -334,7 +344,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + ",\n mediators=" + mediators + ",\n refundAgents=" + refundAgents + - ",\n bannedSignerPubKeys=" + bannedSignerPubKeys + + ",\n bannedAccountWitnessSignerPubKeys=" + bannedSignerPubKeys + + ",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + ",\n creationDate=" + creationDate + ",\n extraDataMap=" + extraDataMap + diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3702495490..4ecd97e304 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2434,7 +2434,8 @@ filterWindow.onions=Filtered onion addresses (comma sep.) filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 8ca9aa4484..eed9ab5353 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -113,8 +113,9 @@ public class FilterWindow extends Overlay { gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); - if (useDevPrivilegeKeys) + if (useDevPrivilegeKeys) { keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); + } InputTextField offerIdsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); InputTextField nodesInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; @@ -125,8 +126,8 @@ public class FilterWindow extends Overlay { InputTextField bannedCurrenciesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedCurrencies")); InputTextField bannedPaymentMethodsInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate - InputTextField bannedSignerPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedSignerPubKeys")).second; - bannedSignerPubKeysInputTextField.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate + InputTextField bannedAccountWitnessSignerPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second; + bannedAccountWitnessSignerPubKeysInputTextField.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); InputTextField refundAgentsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); @@ -138,6 +139,7 @@ public class FilterWindow extends Overlay { CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableDao")); InputTextField disableDaoBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableDaoBelowVersion")); InputTextField disableTradeBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); + InputTextField bannedPrivilegedDevPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; Filter filter = filterManager.getDevFilter(); if (filter != null) { @@ -146,7 +148,7 @@ public class FilterWindow extends Overlay { setupFieldFromPaymentAccountFiltersList(paymentAccountFilterInputTextField, filter.getBannedPaymentAccounts()); setupFieldFromList(bannedCurrenciesInputTextField, filter.getBannedCurrencies()); setupFieldFromList(bannedPaymentMethodsInputTextField, filter.getBannedPaymentMethods()); - setupFieldFromList(bannedSignerPubKeysInputTextField, filter.getBannedSignerPubKeys()); + setupFieldFromList(bannedAccountWitnessSignerPubKeysInputTextField, filter.getBannedSignerPubKeys()); setupFieldFromList(arbitratorsInputTextField, filter.getArbitrators()); setupFieldFromList(mediatorsInputTextField, filter.getMediators()); setupFieldFromList(refundAgentsInputTextField, filter.getRefundAgents()); @@ -154,6 +156,8 @@ public class FilterWindow extends Overlay { setupFieldFromList(seedNodesInputTextField, filter.getSeedNodes()); setupFieldFromList(priceRelayNodesInputTextField, filter.getPriceRelayNodes()); setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); + setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); + setupFieldFromList(bannedPrivilegedDevPubKeysInputTextField, filter.getBannedPrivilegedDevPubKeys()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); disableDaoCheckBox.setSelected(filter.isDisableDao()); @@ -185,10 +189,11 @@ public class FilterWindow extends Overlay { disableTradeBelowVersionInputTextField.getText(), readAsList(mediatorsInputTextField), readAsList(refundAgentsInputTextField), - readAsList(bannedSignerPubKeysInputTextField), + readAsList(bannedAccountWitnessSignerPubKeysInputTextField), readAsList(btcFeeReceiverAddressesInputTextField), filterManager.getOwnerPubKey(), - signerPubKeyAsHex + signerPubKeyAsHex, + readAsList(bannedPrivilegedDevPubKeysInputTextField) ); filterManager.addDevFilter(newFilter, privKeyString); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index c1e8de2f5a..b90433bd8e 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -640,6 +640,7 @@ message Filter { repeated string btc_fee_receiver_addresses = 20; int64 creation_date = 21; string signer_pub_key_as_hex = 22; + repeated string bannedPrivilegedDevPubKeys = 23; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older From 4a2f7dc92c7d11fc4dd2c07cce1c28ae8f7d91cf Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 13:01:44 -0500 Subject: [PATCH 45/52] Rename bannedSignerPubKeys to bannedAccountWitnessSignerPubKeys We use several signing keys, so better to make it more explicit. protobuf entry is not changed to not risk issues (i guess it would work renaming it) --- .../main/java/bisq/core/filter/Filter.java | 18 +-- .../java/bisq/core/filter/FilterManager.java | 4 +- .../main/overlays/windows/FilterWindow.java | 139 ++++++++++-------- 3 files changed, 90 insertions(+), 71 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index ff63336364..d2f8982432 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -71,7 +71,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final List mediators; private final List refundAgents; - private final List bannedSignerPubKeys; + private final List bannedAccountWitnessSignerPubKeys; private final List btcFeeReceiverAddresses; @@ -104,7 +104,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getDisableTradeBelowVersion(), filter.getMediators(), filter.getRefundAgents(), - filter.getBannedSignerPubKeys(), + filter.getBannedAccountWitnessSignerPubKeys(), filter.getBtcFeeReceiverAddresses(), filter.getOwnerPubKeyBytes(), filter.getCreationDate(), @@ -131,7 +131,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { filter.getDisableTradeBelowVersion(), filter.getMediators(), filter.getRefundAgents(), - filter.getBannedSignerPubKeys(), + filter.getBannedAccountWitnessSignerPubKeys(), filter.getBtcFeeReceiverAddresses(), filter.getOwnerPubKeyBytes(), filter.getCreationDate(), @@ -156,7 +156,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { String disableTradeBelowVersion, List mediators, List refundAgents, - List bannedSignerPubKeys, + List bannedAccountWitnessSignerPubKeys, List btcFeeReceiverAddresses, PublicKey ownerPubKey, String signerPubKeyAsHex, @@ -176,7 +176,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { disableTradeBelowVersion, mediators, refundAgents, - bannedSignerPubKeys, + bannedAccountWitnessSignerPubKeys, btcFeeReceiverAddresses, Sig.getPublicKeyBytes(ownerPubKey), System.currentTimeMillis(), @@ -207,7 +207,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { String disableTradeBelowVersion, List mediators, List refundAgents, - List bannedSignerPubKeys, + List bannedAccountWitnessSignerPubKeys, List btcFeeReceiverAddresses, byte[] ownerPubKeyBytes, long creationDate, @@ -230,7 +230,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.disableTradeBelowVersion = disableTradeBelowVersion; this.mediators = mediators; this.refundAgents = refundAgents; - this.bannedSignerPubKeys = bannedSignerPubKeys; + this.bannedAccountWitnessSignerPubKeys = bannedAccountWitnessSignerPubKeys; this.btcFeeReceiverAddresses = btcFeeReceiverAddresses; this.ownerPubKeyBytes = ownerPubKeyBytes; this.creationDate = creationDate; @@ -268,7 +268,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { .setDisableTradeBelowVersion(disableTradeBelowVersion) .addAllMediators(mediators) .addAllRefundAgents(refundAgents) - .addAllBannedSignerPubKeys(bannedSignerPubKeys) + .addAllBannedSignerPubKeys(bannedAccountWitnessSignerPubKeys) .addAllBtcFeeReceiverAddresses(btcFeeReceiverAddresses) .setOwnerPubKeyBytes(ByteString.copyFrom(ownerPubKeyBytes)) .setSignerPubKeyAsHex(signerPubKeyAsHex) @@ -344,7 +344,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { ",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' + ",\n mediators=" + mediators + ",\n refundAgents=" + refundAgents + - ",\n bannedAccountWitnessSignerPubKeys=" + bannedSignerPubKeys + + ",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys + ",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys + ",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses + ",\n creationDate=" + creationDate + diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 0ebd0a88dd..8bf547effd 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -386,8 +386,8 @@ public class FilterManager { public boolean isWitnessSignerPubKeyBanned(String witnessSignerPubKeyAsHex) { return getFilter() != null && - getFilter().getBannedSignerPubKeys() != null && - getFilter().getBannedSignerPubKeys().stream() + getFilter().getBannedAccountWitnessSignerPubKeys() != null && + getFilter().getBannedAccountWitnessSignerPubKeys().stream() .anyMatch(e -> e.equals(witnessSignerPubKeyAsHex)); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index eed9ab5353..d6a5cd151e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -112,57 +112,76 @@ public class FilterWindow extends Overlay { gridPane.getColumnConstraints().remove(1); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); - InputTextField keyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("shared.unlock"), 10); + InputTextField keyTF = addInputTextField(gridPane, ++rowIndex, + Res.get("shared.unlock"), 10); if (useDevPrivilegeKeys) { - keyInputTextField.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); + keyTF.setText(DevEnv.DEV_PRIVILEGE_PRIV_KEY); } - InputTextField offerIdsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); - InputTextField nodesInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; - nodesInputTextField.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate - InputTextField paymentAccountFilterInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.accounts")).second; - GridPane.setHalignment(paymentAccountFilterInputTextField, HPos.RIGHT); - paymentAccountFilterInputTextField.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate - InputTextField bannedCurrenciesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedCurrencies")); - InputTextField bannedPaymentMethodsInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; - bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate - InputTextField bannedAccountWitnessSignerPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second; - bannedAccountWitnessSignerPubKeysInputTextField.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate - InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); - InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); - InputTextField refundAgentsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); - InputTextField btcFeeReceiverAddressesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcFeeReceiverAddresses")); - InputTextField seedNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); - InputTextField priceRelayNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); - InputTextField btcNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcNode")); - CheckBox preventPublicBtcNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.preventPublicBtcNetwork")); - CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, Res.get("filterWindow.disableDao")); - InputTextField disableDaoBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableDaoBelowVersion")); - InputTextField disableTradeBelowVersionInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.disableTradeBelowVersion")); - InputTextField bannedPrivilegedDevPubKeysInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; + InputTextField offerIdsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.offers")); + InputTextField nodesTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.onions")).second; + nodesTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate + InputTextField paymentAccountFilterTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.accounts")).second; + GridPane.setHalignment(paymentAccountFilterTF, HPos.RIGHT); + paymentAccountFilterTF.setPromptText("E.g. PERFECT_MONEY|getAccountNr|12345"); // Do not translate + InputTextField bannedCurrenciesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedCurrencies")); + InputTextField bannedPaymentMethodsTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedPaymentMethods")).second; + bannedPaymentMethodsTF.setPromptText("E.g. PERFECT_MONEY"); // Do not translate + InputTextField bannedAccountWitnessSignerPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedAccountWitnessSignerPubKeys")).second; + bannedAccountWitnessSignerPubKeysTF.setPromptText("E.g. 7f66117aa084e5a2c54fe17d29dd1fee2b241257"); // Do not translate + InputTextField arbitratorsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.arbitrators")); + InputTextField mediatorsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.mediators")); + InputTextField refundAgentsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.refundAgents")); + InputTextField btcFeeReceiverAddressesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.btcFeeReceiverAddresses")); + InputTextField seedNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.seedNode")); + InputTextField priceRelayNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.priceRelayNode")); + InputTextField btcNodesTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.btcNode")); + CheckBox preventPublicBtcNetworkCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.preventPublicBtcNetwork")); + CheckBox disableDaoCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.disableDao")); + InputTextField disableDaoBelowVersionTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.disableDaoBelowVersion")); + InputTextField disableTradeBelowVersionTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.disableTradeBelowVersion")); + InputTextField bannedPrivilegedDevPubKeysTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; Filter filter = filterManager.getDevFilter(); if (filter != null) { - setupFieldFromList(offerIdsInputTextField, filter.getBannedOfferIds()); - setupFieldFromList(nodesInputTextField, filter.getBannedNodeAddress()); - setupFieldFromPaymentAccountFiltersList(paymentAccountFilterInputTextField, filter.getBannedPaymentAccounts()); - setupFieldFromList(bannedCurrenciesInputTextField, filter.getBannedCurrencies()); - setupFieldFromList(bannedPaymentMethodsInputTextField, filter.getBannedPaymentMethods()); - setupFieldFromList(bannedAccountWitnessSignerPubKeysInputTextField, filter.getBannedSignerPubKeys()); - setupFieldFromList(arbitratorsInputTextField, filter.getArbitrators()); - setupFieldFromList(mediatorsInputTextField, filter.getMediators()); - setupFieldFromList(refundAgentsInputTextField, filter.getRefundAgents()); - setupFieldFromList(btcFeeReceiverAddressesInputTextField, filter.getBtcFeeReceiverAddresses()); - setupFieldFromList(seedNodesInputTextField, filter.getSeedNodes()); - setupFieldFromList(priceRelayNodesInputTextField, filter.getPriceRelayNodes()); - setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); - setupFieldFromList(btcNodesInputTextField, filter.getBtcNodes()); - setupFieldFromList(bannedPrivilegedDevPubKeysInputTextField, filter.getBannedPrivilegedDevPubKeys()); + setupFieldFromList(offerIdsTF, filter.getBannedOfferIds()); + setupFieldFromList(nodesTF, filter.getBannedNodeAddress()); + setupFieldFromPaymentAccountFiltersList(paymentAccountFilterTF, filter.getBannedPaymentAccounts()); + setupFieldFromList(bannedCurrenciesTF, filter.getBannedCurrencies()); + setupFieldFromList(bannedPaymentMethodsTF, filter.getBannedPaymentMethods()); + setupFieldFromList(bannedAccountWitnessSignerPubKeysTF, filter.getBannedAccountWitnessSignerPubKeys()); + setupFieldFromList(arbitratorsTF, filter.getArbitrators()); + setupFieldFromList(mediatorsTF, filter.getMediators()); + setupFieldFromList(refundAgentsTF, filter.getRefundAgents()); + setupFieldFromList(btcFeeReceiverAddressesTF, filter.getBtcFeeReceiverAddresses()); + setupFieldFromList(seedNodesTF, filter.getSeedNodes()); + setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes()); + setupFieldFromList(btcNodesTF, filter.getBtcNodes()); + setupFieldFromList(btcNodesTF, filter.getBtcNodes()); + setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); disableDaoCheckBox.setSelected(filter.isDisableDao()); - disableDaoBelowVersionInputTextField.setText(filter.getDisableDaoBelowVersion()); - disableTradeBelowVersionInputTextField.setText(filter.getDisableTradeBelowVersion()); + disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion()); + disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion()); } Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); @@ -170,30 +189,30 @@ public class FilterWindow extends Overlay { Button sendButton = new AutoTooltipButton(Res.get("filterWindow.add")); sendButton.setOnAction(e -> { - String privKeyString = keyInputTextField.getText(); + String privKeyString = keyTF.getText(); if (filterManager.canAddDevFilter(privKeyString)) { String signerPubKeyAsHex = filterManager.getSignerPubKeyAsHex(privKeyString); Filter newFilter = new Filter( - readAsList(offerIdsInputTextField), - readAsList(nodesInputTextField), - readAsPaymentAccountFiltersList(paymentAccountFilterInputTextField), - readAsList(bannedCurrenciesInputTextField), - readAsList(bannedPaymentMethodsInputTextField), - readAsList(arbitratorsInputTextField), - readAsList(seedNodesInputTextField), - readAsList(priceRelayNodesInputTextField), + readAsList(offerIdsTF), + readAsList(nodesTF), + readAsPaymentAccountFiltersList(paymentAccountFilterTF), + readAsList(bannedCurrenciesTF), + readAsList(bannedPaymentMethodsTF), + readAsList(arbitratorsTF), + readAsList(seedNodesTF), + readAsList(priceRelayNodesTF), preventPublicBtcNetworkCheckBox.isSelected(), - readAsList(btcNodesInputTextField), + readAsList(btcNodesTF), disableDaoCheckBox.isSelected(), - disableDaoBelowVersionInputTextField.getText(), - disableTradeBelowVersionInputTextField.getText(), - readAsList(mediatorsInputTextField), - readAsList(refundAgentsInputTextField), - readAsList(bannedAccountWitnessSignerPubKeysInputTextField), - readAsList(btcFeeReceiverAddressesInputTextField), + disableDaoBelowVersionTF.getText(), + disableTradeBelowVersionTF.getText(), + readAsList(mediatorsTF), + readAsList(refundAgentsTF), + readAsList(bannedAccountWitnessSignerPubKeysTF), + readAsList(btcFeeReceiverAddressesTF), filterManager.getOwnerPubKey(), signerPubKeyAsHex, - readAsList(bannedPrivilegedDevPubKeysInputTextField) + readAsList(bannedPrivilegedDevPubKeysTF) ); filterManager.addDevFilter(newFilter, privKeyString); @@ -205,7 +224,7 @@ public class FilterWindow extends Overlay { }); removeFilterMessageButton.setOnAction(e -> { - String privKeyString = keyInputTextField.getText(); + String privKeyString = keyTF.getText(); if (filterManager.canRemoveDevFilter(privKeyString)) { filterManager.removeDevFilter(privKeyString); hide(); From 78142bda7dfcfd86e1ba73244609bf004f228334 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 13:23:51 -0500 Subject: [PATCH 46/52] Fix tests --- core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java | 1 + core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 050fce18ec..b2bf0fc645 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -61,6 +61,7 @@ public class UserPayloadModelVOTest { 0, null, null, + null, null)); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index e5319f8ecd..a8488e9c1e 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -120,6 +120,7 @@ public class FeeReceiverSelectorTest { 0, null, null, + null, null); } } From 5209018f4d719d09046fc45fb751372891ceec48 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 13:24:02 -0500 Subject: [PATCH 47/52] Add pub key --- core/src/main/java/bisq/core/filter/FilterManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 8bf547effd..2c59f7e013 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -122,7 +122,8 @@ public class FilterManager { publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : List.of("0358d47858acdc41910325fce266571540681ef83a0d6fedce312bef9810793a27", - "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f"); + "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", + "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); } From c755fc2592b90d5b17264b333c845142a84c3852 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 13:51:41 -0500 Subject: [PATCH 48/52] Add isPrivilegedDevPubKeyBanned method It is used atm only for filter itself which might have limited effect. Applying it to alert and priv notification requires adding pubKey fields there which is outside of the scope of that PR but should be added soon as well. --- .../java/bisq/core/filter/FilterManager.java | 106 +++++++++++++----- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 2c59f7e013..37cfd90431 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -141,8 +141,6 @@ public class FilterManager { .map(ProtectedStorageEntry::getProtectedStoragePayload) .filter(protectedStoragePayload -> protectedStoragePayload instanceof Filter) .map(protectedStoragePayload -> (Filter) protectedStoragePayload) - .filter(this::isFilterPublicKeyInList) - .filter(this::verifySignature) .forEach(this::onFilterAddedFromNetwork); p2PService.addHashSetChangedListener(new HashMapChangedListener() { @@ -152,9 +150,7 @@ public class FilterManager { .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (isFilterPublicKeyInList(filter) && verifySignature(filter)) { - onFilterAddedFromNetwork(filter); - } + onFilterAddedFromNetwork(filter); }); } @@ -164,9 +160,7 @@ public class FilterManager { .filter(protectedStorageEntry -> protectedStorageEntry.getProtectedStoragePayload() instanceof Filter) .forEach(protectedStorageEntry -> { Filter filter = (Filter) protectedStorageEntry.getProtectedStoragePayload(); - if (isFilterPublicKeyInList(filter) && verifySignature(filter)) { - onFilterRemovedFromNetwork(filter); - } + onFilterRemovedFromNetwork(filter); }); } }); @@ -212,6 +206,15 @@ public class FilterManager { }); } + public boolean isPrivilegedDevPubKeyBanned(String pubKeyAsHex) { + Filter filter = getFilter(); + if (filter == null) { + return false; + } + + return filter.getBannedPrivilegedDevPubKeys().contains(pubKeyAsHex); + } + public boolean canAddDevFilter(String privKeyString) { if (privKeyString == null || privKeyString.isEmpty()) { return false; @@ -220,6 +223,13 @@ public class FilterManager { log.warn("There is no persisted dev filter to be removed."); return false; } + + ECKey ecKeyFromPrivate = toECKey(privKeyString); + String pubKeyAsHex = getPubKeyAsHex(ecKeyFromPrivate); + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } return true; } @@ -268,6 +278,11 @@ public class FilterManager { return false; } + if (isPrivilegedDevPubKeyBanned(pubKeyAsHex)) { + log.warn("Pub key is banned."); + return false; + } + return true; } @@ -397,43 +412,74 @@ public class FilterManager { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void onFilterAddedFromNetwork(Filter filter) { - if (filterProperty.get() != null && filterProperty.get().getCreationDate() > filter.getCreationDate()) { - log.warn("We received a new filter from the network but the creation date is older than the " + - "filter we have already. We ignore the new filter.\n" + - "New filer={}\n" + - "Old filter={}", - filter, filterProperty.get()); + private void onFilterAddedFromNetwork(Filter newFilter) { + Filter currentFilter = getFilter(); + + if (!isFilterPublicKeyInList(newFilter)) { + log.warn("isFilterPublicKeyInList failed. Filter={}", newFilter); return; } + if (!verifySignature(newFilter)) { + log.warn("verifySignature failed. Filter={}", newFilter); + return; + } + + if (currentFilter != null) { + if (currentFilter.getCreationDate() > newFilter.getCreationDate()) { + log.warn("We received a new filter from the network but the creation date is older than the " + + "filter we have already. We ignore the new filter.\n" + + "New filer={}\n" + + "Old filter={}", + newFilter, filterProperty.get()); + return; + } + + if (isPrivilegedDevPubKeyBanned(newFilter.getSignerPubKeyAsHex())) { + log.warn("Pub key of filter is banned. currentFilter={}, newFilter={}", currentFilter, newFilter); + return; + } + } // Our new filter is newer so we apply it. // We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key // for deploying filters and this is only in place to avoid unintended situations of multiple filters // from multiple devs or if same dev publishes new filter from different app without the persisted devFilter. - filterProperty.set(filter); + filterProperty.set(newFilter); // Seed nodes are requested at startup before we get the filter so we only apply the banned // nodes at the next startup and don't update the list in the P2P network domain. // We persist it to the property file which is read before any other initialisation. - saveBannedNodes(BANNED_SEED_NODES, filter.getSeedNodes()); - saveBannedNodes(BANNED_BTC_NODES, filter.getBtcNodes()); + saveBannedNodes(BANNED_SEED_NODES, newFilter.getSeedNodes()); + saveBannedNodes(BANNED_BTC_NODES, newFilter.getBtcNodes()); // Banned price relay nodes we can apply at runtime - List priceRelayNodes = filter.getPriceRelayNodes(); + List priceRelayNodes = newFilter.getPriceRelayNodes(); saveBannedNodes(BANNED_PRICE_RELAY_NODES, priceRelayNodes); + + //TODO should be moved to client with listening on onFilterAdded providersRepository.applyBannedNodes(priceRelayNodes); - if (filter.isPreventPublicBtcNetwork() && + //TODO should be moved to client with listening on onFilterAdded + if (newFilter.isPreventPublicBtcNetwork() && preferences.getBitcoinNodesOptionOrdinal() == BtcNodes.BitcoinNodesOption.PUBLIC.ordinal()) { preferences.setBitcoinNodesOptionOrdinal(BtcNodes.BitcoinNodesOption.PROVIDED.ordinal()); } - listeners.forEach(e -> e.onFilterAdded(filter)); + listeners.forEach(e -> e.onFilterAdded(newFilter)); } - // We clean up potentially banned nodes and set value of filter property to null private void onFilterRemovedFromNetwork(Filter filter) { + if (!isFilterPublicKeyInList(filter)) { + log.warn("isFilterPublicKeyInList failed. Filter={}", filter); + return; + } + if (!verifySignature(filter)) { + log.warn("verifySignature failed. Filter={}", filter); + return; + } + + // We don't check for banned filter as we want to remove a banned filter anyway. + if (!filterProperty.get().equals(filter)) { return; } @@ -494,6 +540,14 @@ public class FilterManager { return true; } + private boolean isPublicKeyInList(String pubKeyAsHex) { + boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); + if (!isPublicKeyInList) { + log.warn("pubKeyAsHex is not part of our pub key list. pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); + } + return isPublicKeyInList; + } + private boolean verifySignature(Filter filter) { try { Filter filterForSigVerification = Filter.cloneWithoutSig(filter); @@ -525,12 +579,4 @@ public class FilterManager { private String getPubKeyAsHex(ECKey ecKey) { return HEX.encode(ecKey.getPubKey()); } - - private boolean isPublicKeyInList(String pubKeyAsHex) { - boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex); - if (!isPublicKeyInList) { - log.warn("pubKeyAsHex is not part of our pub key list. pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys); - } - return isPublicKeyInList; - } } From 334cb43c3b0805c277bf766204c8c5cd5b17aaa5 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 13:53:17 -0500 Subject: [PATCH 49/52] Rename verifySignature to isSignatureValid --- core/src/main/java/bisq/core/filter/FilterManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 37cfd90431..2ecee114c0 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -419,7 +419,7 @@ public class FilterManager { log.warn("isFilterPublicKeyInList failed. Filter={}", newFilter); return; } - if (!verifySignature(newFilter)) { + if (!isSignatureValid(newFilter)) { log.warn("verifySignature failed. Filter={}", newFilter); return; } @@ -473,7 +473,7 @@ public class FilterManager { log.warn("isFilterPublicKeyInList failed. Filter={}", filter); return; } - if (!verifySignature(filter)) { + if (!isSignatureValid(filter)) { log.warn("verifySignature failed. Filter={}", filter); return; } @@ -548,7 +548,7 @@ public class FilterManager { return isPublicKeyInList; } - private boolean verifySignature(Filter filter) { + private boolean isSignatureValid(Filter filter) { try { Filter filterForSigVerification = Filter.cloneWithoutSig(filter); Sha256Hash hash = getSha256Hash(filterForSigVerification); From 8bf75b5f4537d1cc107f59d1e1c7cce96efee36f Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 15:19:31 -0500 Subject: [PATCH 50/52] - Filter out prices with code "NON_EXISTING_SYMBOL" - Add comments --- .../core/provider/price/PriceProvider.java | 19 +++++++++++++++++-- .../main/java/bisq/desktop/main/MainView.java | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java index 184be90435..bc29ecc382 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceProvider.java +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -70,12 +70,24 @@ public class PriceProvider extends HttpClientProvider { final double price = (Double) treeMap.get("price"); // json uses double for our timestampSec long value... final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); + + // We do not have support for the case of multiChain assets where a common price ticker used for + // different flavours of the asset. It would be quite a bit of effort to add generic support to the + // asset and tradeCurrency classes and to handle it correctly from their many client classes. + // So we decided to hack in the sub-assets as copies of the price and accept the annoyance to see + // 3 different prices for the same master asset. But who knows, maybe prices will differ over time for + // the sub assets so then we are better prepared that way... if (currencyCode.equals("USDT")) { addPrice(marketPriceMap, "USDT-O", price, timestampSec); addPrice(marketPriceMap, "USDT-E", price, timestampSec); addPrice(marketPriceMap, "L-USDT", price, timestampSec); + } else { + // NON_EXISTING_SYMBOL is returned from service for nto found items + // Sometimes it has post fixes as well so we use a 'contains' check. + if (!currencyCode.contains("NON_EXISTING_SYMBOL")) { + addPrice(marketPriceMap, currencyCode, price, timestampSec); + } } - addPrice(marketPriceMap, currencyCode, price, timestampSec); } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); @@ -85,7 +97,10 @@ public class PriceProvider extends HttpClientProvider { return new Tuple2<>(tsMap, marketPriceMap); } - private void addPrice(Map marketPriceMap, String currencyCode, double price, long timestampSec) { + private void addPrice(Map marketPriceMap, + String currencyCode, + double price, + long timestampSec) { marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 7c13febae5..abbe3b641a 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -43,12 +43,12 @@ import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.Transitions; import bisq.core.dao.monitoring.DaoStateMonitoringService; -import bisq.common.BisqException; import bisq.core.locale.GlobalSettings; import bisq.core.locale.LanguageUtil; import bisq.core.locale.Res; import bisq.core.provider.price.MarketPrice; +import bisq.common.BisqException; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.Version; @@ -532,6 +532,7 @@ public class MainView extends InitializableView String selectedCurrencyCode = model.getPriceFeedService().getCurrencyCode(); MarketPrice selectedMarketPrice = model.getPriceFeedService().getMarketPrice(selectedCurrencyCode); + return Res.get("mainView.marketPrice.tooltip", "Bisq Price Index for " + selectedCurrencyCode, "", From 86999e52d1470ea047ebfffa156f15d1e782b654 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 31 Aug 2020 17:13:09 -0500 Subject: [PATCH 51/52] - Remove isPublicKeyInList check at remove as its irrelevant (dev could add his key anyway to src code) - Fix wrong logs - Remove duplicated line --- core/src/main/java/bisq/core/filter/FilterManager.java | 10 ++-------- .../desktop/main/overlays/windows/FilterWindow.java | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 2ecee114c0..0302fc24f7 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -220,7 +220,7 @@ public class FilterManager { return false; } if (!isValidDevPrivilegeKey(privKeyString)) { - log.warn("There is no persisted dev filter to be removed."); + log.warn("Key in invalid"); return false; } @@ -258,14 +258,8 @@ public class FilterManager { return false; } - if (!isPublicKeyInList(developersFilter.getSignerPubKeyAsHex())) { - log.warn("The SignerPubKey from the filter is not in our pubKey list. " + - "filterSignerPubKey={}, publicKeys={}", developersFilter.getSignerPubKeyAsHex(), publicKeys); - return false; - } - if (!isValidDevPrivilegeKey(privKeyString)) { - log.warn("There is no persisted dev filter to be removed."); + log.warn("Key in invalid."); return false; } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index d6a5cd151e..cad11360b6 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -175,7 +175,6 @@ public class FilterWindow extends Overlay { setupFieldFromList(seedNodesTF, filter.getSeedNodes()); setupFieldFromList(priceRelayNodesTF, filter.getPriceRelayNodes()); setupFieldFromList(btcNodesTF, filter.getBtcNodes()); - setupFieldFromList(btcNodesTF, filter.getBtcNodes()); setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); From 3ef8abd972bd1598d8c966be52f57094495ba64d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 1 Sep 2020 10:25:54 -0500 Subject: [PATCH 52/52] Remove unused constructor --- .../main/java/bisq/asset/LiquidBitcoinAddressValidator.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java index b8861bb108..9ae5cf41d4 100644 --- a/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java +++ b/assets/src/main/java/bisq/asset/LiquidBitcoinAddressValidator.java @@ -2,11 +2,8 @@ package bisq.asset; public class LiquidBitcoinAddressValidator extends RegexAddressValidator { static private final String REGEX = "^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,87})$"; - public LiquidBitcoinAddressValidator() { - super(REGEX); - } - public LiquidBitcoinAddressValidator(String regex, String errorMessageI18nKey) { + public LiquidBitcoinAddressValidator() { super(REGEX, "validation.altcoin.liquidBitcoin.invalidAddress"); } }