diff --git a/build.gradle b/build.gradle index 92e2892da5..d548e5a3a2 100644 --- a/build.gradle +++ b/build.gradle @@ -339,6 +339,8 @@ configure(project(':monitor')) { } dependencies { + compile project(':p2p') + compile project(':core') compile "org.slf4j:slf4j-api:$slf4jVersion" compile "ch.qos.logback:logback-core:$logbackVersion" compile "ch.qos.logback:logback-classic:$logbackVersion" diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java new file mode 100644 index 0000000000..dbcab390a1 --- /dev/null +++ b/common/src/main/java/bisq/common/util/PermutationUtil.java @@ -0,0 +1,107 @@ +/* + * 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.common.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PermutationUtil { + + /** + * @param list Original list + * @param indicesToRemove List of indices to remove + * @param Type of List items + * @return Partial list where items at indices of indicesToRemove have been removed + */ + public static List getPartialList(List list, List indicesToRemove) { + List altered = new ArrayList<>(list); + + // Eliminate duplicates + indicesToRemove = new ArrayList<>(new HashSet<>(indicesToRemove)); + + // Sort + Collections.sort(indicesToRemove); + + // Reverse list. + // We need to remove from highest index downwards to not change order of remaining indices + Collections.reverse(indicesToRemove); + + indicesToRemove.forEach(index -> { + if (altered.size() > index && index >= 0) + altered.remove((int) index); + }); + return altered; + } + + /** + * Returns a list of all possible permutations of a give sorted list ignoring duplicates. + * E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]] + * Number of variations and iterations grows with 2^n - 1 where n is the number of items in the list. + * With 20 items we reach about 1 million iterations and it takes about 0.5 sec. + * To avoid performance issues we added the maxIterations parameter to stop once the number of iterations has + * reached the maxIterations and return in such a case the list of permutations we have been able to create. + * Depending on the type of object which is stored in the list the memory usage should be considered as well for + * choosing the right maxIterations value. + * + * @param list List from which we create permutations + * @param maxIterations Max. number of iterations including inner iterations + * @param Type of list items + * @return List of possible permutations of the original list + */ + public static List> findAllPermutations(List list, int maxIterations) { + List> result = new ArrayList<>(); + int counter = 0; + long ts = System.currentTimeMillis(); + for (T item : list) { + counter++; + if (counter > maxIterations) { + log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + + "counter={}", counter); + return result; + } + + List> subLists = new ArrayList<>(); + for (int n = 0; n < result.size(); n++) { + counter++; + if (counter > maxIterations) { + log.warn("We reached maxIterations of our allowed iterations and return current state of the result. " + + "counter={}", counter); + return result; + } + List subList = new ArrayList<>(result.get(n)); + subList.add(item); + subLists.add(subList); + } + + // add single item + result.add(new ArrayList<>(Collections.singletonList(item))); + + // add subLists + result.addAll(subLists); + } + + log.info("findAllPermutations took {} ms for {} items and {} iterations. Heap size used: {} MB", + (System.currentTimeMillis() - ts), list.size(), counter, Profiler.getUsedMemoryInMB()); + return result; + } +} diff --git a/common/src/test/java/bisq/common/util/PermutationTest.java b/common/src/test/java/bisq/common/util/PermutationTest.java new file mode 100644 index 0000000000..149cf9c971 --- /dev/null +++ b/common/src/test/java/bisq/common/util/PermutationTest.java @@ -0,0 +1,432 @@ +/* + * 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.common.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class PermutationTest { + + + @Test + public void testGetPartialList() { + String blindVote0 = "blindVote0"; + String blindVote1 = "blindVote1"; + String blindVote2 = "blindVote2"; + String blindVote3 = "blindVote3"; + String blindVote4 = "blindVote4"; + String blindVote5 = "blindVote5"; + + List list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); + List indicesToRemove = Arrays.asList(0, 3); + List expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote4, blindVote5)); + List result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove nothing + indicesToRemove = new ArrayList<>(); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove first + indicesToRemove = Collections.singletonList(0); + expected = new ArrayList<>(list); + expected.remove(0); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove last + indicesToRemove = Collections.singletonList(5); + expected = new ArrayList<>(list); + expected.remove(5); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // remove all + indicesToRemove = Arrays.asList(0, 1, 2, 3, 4, 5); + expected = new ArrayList<>(); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // wrong sorting of indices + indicesToRemove = Arrays.asList(4, 0, 1); + expected = expected = new ArrayList<>(Arrays.asList(blindVote2, blindVote3, blindVote5)); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // wrong sorting of indices + indicesToRemove = Arrays.asList(0, 0); + expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote3, blindVote4, blindVote5)); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // don't remove as invalid index + indicesToRemove = Collections.singletonList(9); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + + // don't remove as invalid index + indicesToRemove = Collections.singletonList(-2); + expected = new ArrayList<>(list); + result = PermutationUtil.getPartialList(list, indicesToRemove); + assertTrue(expected.toString().equals(result.toString())); + } + + @Test + public void testFindAllPermutations() { + String blindVote0 = "blindVote0"; + String blindVote1 = "blindVote1"; + String blindVote2 = "blindVote2"; + String blindVote3 = "blindVote3"; + String blindVote4 = "blindVote4"; + + // Up to about 1M iterations performance is acceptable (0.5 sec) + // findAllPermutations took 580 ms for 20 items and 1048575 iterations + // findAllPermutations took 10 ms for 15 items and 32767 iterations + // findAllPermutations took 0 ms for 10 items and 1023 iterations + int limit = 1048575; + List list; + List> expected; + List> result; + List subList; + + + /* list = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + list.add("blindVote"+i); + } + PermutationUtil.findAllPermutations(list, limit);*/ + + + list = new ArrayList<>(); + expected = new ArrayList<>(); + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + list = new ArrayList<>(Arrays.asList(blindVote0)); + expected = new ArrayList<>(); + expected.add(list); + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 2 items -> 3 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 3 items -> 7 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + // 4 items -> 15 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(3))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + + // 5 items -> 31 variations + list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4)); + expected = new ArrayList<>(); + expected.add(Arrays.asList(list.get(0))); + + expected.add(Arrays.asList(list.get(1))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(2))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(3))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + expected.add(subList); + + expected.add(Arrays.asList(list.get(4))); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + subList = new ArrayList<>(); + subList.add(list.get(0)); + subList.add(list.get(1)); + subList.add(list.get(2)); + subList.add(list.get(3)); + subList.add(list.get(4)); + expected.add(subList); + + result = PermutationUtil.findAllPermutations(list, limit); + assertTrue(expected.toString().equals(result.toString())); + + + } + + +} diff --git a/core/src/main/java/bisq/core/CoreModule.java b/core/src/main/java/bisq/core/CoreModule.java index db41abc221..6058ba71f3 100644 --- a/core/src/main/java/bisq/core/CoreModule.java +++ b/core/src/main/java/bisq/core/CoreModule.java @@ -23,6 +23,7 @@ import bisq.core.app.AvoidStandbyModeService; import bisq.core.app.BisqEnvironment; import bisq.core.app.BisqSetup; import bisq.core.app.P2PNetworkSetup; +import bisq.core.app.TorSetup; import bisq.core.app.WalletAppSetup; import bisq.core.arbitration.ArbitratorModule; import bisq.core.btc.BitcoinModule; @@ -80,6 +81,7 @@ public class CoreModule extends AppModule { @Override protected void configure() { bind(BisqSetup.class).in(Singleton.class); + bind(TorSetup.class).in(Singleton.class); bind(P2PNetworkSetup.class).in(Singleton.class); bind(WalletAppSetup.class).in(Singleton.class); diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 3bb23f40f2..f7ece906fb 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -78,26 +78,26 @@ public class BisqHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); - bisqSetup.setCryptoSetupFailedHandler(msg -> log.info("onCryptoSetupFailedHandler: msg={}", msg)); + bisqSetup.setCryptoSetupFailedHandler(msg -> log.error("onCryptoSetupFailedHandler: msg={}", msg)); bisqSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); - bisqSetup.setSpvFileCorruptedHandler(msg -> log.info("onSpvFileCorruptedHandler: msg={}", msg)); - bisqSetup.setChainFileLockedExceptionHandler(msg -> log.info("onChainFileLockedExceptionHandler: msg={}", msg)); + bisqSetup.setSpvFileCorruptedHandler(msg -> log.error("onSpvFileCorruptedHandler: msg={}", msg)); + bisqSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); bisqSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); bisqSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler")); bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> log.info("onRequestWalletPasswordHandler")); bisqSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); bisqSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); bisqSetup.setDisplayPrivateNotificationHandler(privateNotification -> log.info("onDisplayPrivateNotificationHandler. privateNotification={}", privateNotification)); - bisqSetup.setDaoErrorMessageHandler(errorMessage -> log.info("onDaoErrorMessageHandler. errorMessage={}", errorMessage)); - bisqSetup.setDaoWarnMessageHandler(warnMessage -> log.info("onDaoWarnMessageHandler. warnMessage={}", warnMessage)); + bisqSetup.setDaoErrorMessageHandler(errorMessage -> log.error("onDaoErrorMessageHandler. errorMessage={}", errorMessage)); + bisqSetup.setDaoWarnMessageHandler(warnMessage -> log.warn("onDaoWarnMessageHandler. warnMessage={}", warnMessage)); bisqSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); bisqSetup.setDisplayLocalhostHandler(key -> log.info("onDisplayLocalhostHandler")); - bisqSetup.setWrongOSArchitectureHandler(msg -> log.info("onWrongOSArchitectureHandler. msg={}", msg)); - bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.info("voteResultException={}", voteResultException)); + bisqSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); + bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException)); //TODO move to bisqSetup - corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.info("getCorruptedDatabaseFiles. files={}", files)); - tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.info("onTakeOfferRequestErrorMessageHandler")); + corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); + tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("onTakeOfferRequestErrorMessageHandler")); } public void stop() { diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index cb8e6aa997..cbc70acad2 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -150,6 +150,7 @@ public class BisqSetup { private final VoteResultService voteResultService; private final AssetTradeActivityCheck tradeActivityCheck; private final AssetService assetService; + private final TorSetup torSetup; private final BSFormatter formatter; @Setter @Nullable @@ -226,6 +227,7 @@ public class BisqSetup { VoteResultService voteResultService, AssetTradeActivityCheck tradeActivityCheck, AssetService assetService, + TorSetup torSetup, BSFormatter formatter) { @@ -264,6 +266,7 @@ public class BisqSetup { this.voteResultService = voteResultService; this.tradeActivityCheck = tradeActivityCheck; this.assetService = assetService; + this.torSetup = torSetup; this.formatter = formatter; } @@ -286,6 +289,7 @@ public class BisqSetup { } private void step3() { + torSetup.cleanupTorFiles(); readMapsFromResources(); checkCryptoSetup(); checkForCorrectOSArchitecture(); diff --git a/core/src/main/java/bisq/core/app/TorSetup.java b/core/src/main/java/bisq/core/app/TorSetup.java new file mode 100644 index 0000000000..238e23e97a --- /dev/null +++ b/core/src/main/java/bisq/core/app/TorSetup.java @@ -0,0 +1,69 @@ +/* + * 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.core.app; + +import bisq.network.NetworkOptionKeys; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.storage.FileUtil; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class TorSetup { + private File torDir; + + @Inject + public TorSetup(@Named(NetworkOptionKeys.TOR_DIR) File torDir) { + this.torDir = torDir; + } + + public void cleanupTorFiles() { + cleanupTorFiles(null, null); + } + + // We get sometimes Tor startup problems which is related to some tor files in the tor directory. It happens + // more often if the application got killed (not graceful shutdown). + // Creating all tor files newly takes about 3-4 sec. longer and it does not benefit from cache files. + // TODO: We should fix those startup problems in the netlayer library, once fixed there we can remove that call at the + // Bisq startup again. + public void cleanupTorFiles(@Nullable Runnable resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()); + try { + FileUtil.deleteDirectory(torDir, hiddenservice, true); + if (resultHandler != null) + resultHandler.run(); + } catch (IOException e) { + e.printStackTrace(); + log.error(e.toString()); + if (errorMessageHandler != null) + errorMessageHandler.handleErrorMessage(e.toString()); + } + } +} diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java index 89adeacab2..1cb08ab7f8 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java @@ -18,6 +18,7 @@ package bisq.core.app.misc; import bisq.core.app.SetupUtils; +import bisq.core.app.TorSetup; import bisq.core.filter.FilterManager; import bisq.core.payment.AccountAgeWitnessService; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -46,6 +47,7 @@ public class AppSetupWithP2P extends AppSetup { protected final P2PService p2PService; protected final AccountAgeWitnessService accountAgeWitnessService; protected final FilterManager filterManager; + private final TorSetup torSetup; protected BooleanProperty p2pNetWorkReady; protected final TradeStatisticsManager tradeStatisticsManager; protected ArrayList persistedDataHosts; @@ -56,21 +58,24 @@ public class AppSetupWithP2P extends AppSetup { P2PService p2PService, TradeStatisticsManager tradeStatisticsManager, AccountAgeWitnessService accountAgeWitnessService, - FilterManager filterManager) { + FilterManager filterManager, + TorSetup torSetup) { super(encryptionService, keyRing); this.p2PService = p2PService; this.tradeStatisticsManager = tradeStatisticsManager; this.accountAgeWitnessService = accountAgeWitnessService; this.filterManager = filterManager; + this.torSetup = torSetup; this.persistedDataHosts = new ArrayList<>(); } @Override public void initPersistedDataHosts() { + torSetup.cleanupTorFiles(); persistedDataHosts.add(p2PService); // we apply at startup the reading of persisted data but don't want to get it triggered in the constructor - persistedDataHosts.stream().forEach(e -> { + persistedDataHosts.forEach(e -> { try { log.info("call readPersisted at " + e.getClass().getSimpleName()); e.readPersisted(); diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java index 50628e336e..4a2a89c295 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -17,6 +17,7 @@ package bisq.core.app.misc; +import bisq.core.app.TorSetup; import bisq.core.dao.DaoOptionKeys; import bisq.core.dao.DaoSetup; import bisq.core.dao.governance.ballot.BallotListService; @@ -57,13 +58,15 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P { MyProposalListService myProposalListService, MyReputationListService myReputationListService, MyProofOfBurnListService myProofOfBurnListService, + TorSetup torSetup, @Named(DaoOptionKeys.DAO_ACTIVATED) boolean daoActivated) { super(encryptionService, keyRing, p2PService, tradeStatisticsManager, accountAgeWitnessService, - filterManager); + filterManager, + torSetup); this.daoSetup = daoSetup; diff --git a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java index 64cfcb465e..16d1c87ea7 100644 --- a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java @@ -20,6 +20,7 @@ package bisq.core.app.misc; import bisq.core.alert.AlertModule; import bisq.core.app.AppOptionKeys; import bisq.core.app.BisqEnvironment; +import bisq.core.app.TorSetup; import bisq.core.arbitration.ArbitratorModule; import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; @@ -74,6 +75,7 @@ public class ModuleForAppWithP2p extends AppModule { bind(PersistenceProtoResolver.class).to(CorePersistenceProtoResolver.class).in(Singleton.class); bind(Preferences.class).in(Singleton.class); bind(BridgeAddressProvider.class).to(Preferences.class).in(Singleton.class); + bind(TorSetup.class).in(Singleton.class); bind(SeedNodeAddressLookup.class).in(Singleton.class); bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class).in(Singleton.class); diff --git a/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java index 4f4c243c38..63656db1eb 100644 --- a/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java +++ b/core/src/main/java/bisq/core/btc/nodes/BtcNodes.java @@ -68,6 +68,9 @@ public class BtcNodes { new BtcNode("btc1.sqrrm.net", "3r44ddzjitznyahw.onion", "185.25.48.184", BtcNode.DEFAULT_PORT, "@sqrrm"), new BtcNode("btc2.sqrrm.net", "i3a5xtzfm4xwtybd.onion", "81.171.22.143", BtcNode.DEFAULT_PORT, "@sqrrm"), + // KanoczTomas + new BtcNode("btc.ispol.sk", "mbm6ffx6j5ygi2ck.onion", "193.58.196.212", BtcNode.DEFAULT_PORT, "@KanoczTomas"), + // sgeisler new BtcNode("bcwat.ch", "z33nukt7ngik3cpe.onion", "5.189.166.193", BtcNode.DEFAULT_PORT, "@sgeisler"), diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index d2a433cd2b..59f636b75b 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -397,17 +397,17 @@ public class DaoFacade implements DaoSetupService { case PROPOSAL: break; case BREAK1: - firstBlock++; + firstBlock--; break; case BLIND_VOTE: break; case BREAK2: - firstBlock++; + firstBlock--; break; case VOTE_REVEAL: break; case BREAK3: - firstBlock++; + firstBlock--; break; case RESULT: break; diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java index 05541280fe..d27c2c9b13 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteListService.java @@ -46,7 +46,6 @@ import bisq.core.dao.state.model.governance.Proposal; import bisq.network.p2p.P2PService; import bisq.common.UserThread; -import bisq.common.app.DevEnv; import bisq.common.crypto.CryptoException; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ExceptionHandler; @@ -350,7 +349,8 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen private void rePublishOnceWellConnected() { int minPeers = BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? 4 : 1; - if ((p2PService.getNumConnectedPeers().get() > minPeers && p2PService.isBootstrapped()) || DevEnv.isDevMode()) { + if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || + BisqEnvironment.getBaseCurrencyNetwork().isRegtest()) { int chainHeight = periodService.getChainHeight(); myBlindVoteList.stream() .filter(blindVote -> periodService.isTxInPhaseAndCycle(blindVote.getTxId(), diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java index 4dae984c21..1fd63afe18 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalListService.java @@ -225,7 +225,8 @@ public class MyProposalListService implements PersistedDataHost, DaoStateListene private void rePublishOnceWellConnected() { int minPeers = BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? 4 : 1; - if ((p2PService.getNumConnectedPeers().get() > minPeers && p2PService.isBootstrapped())) { + if ((p2PService.getNumConnectedPeers().get() >= minPeers && p2PService.isBootstrapped()) || + BisqEnvironment.getBaseCurrencyNetwork().isRegtest()) { p2PService.getNumConnectedPeers().removeListener(numConnectedPeersListener); rePublish(); } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java index 238a93d1be..97807b5dd4 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java @@ -74,7 +74,6 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt // different data collections due the eventually consistency of the P2P network. @Getter private final ObservableList proposalPayloads = FXCollections.observableArrayList(); - private boolean parsingComplete; /////////////////////////////////////////////////////////////////////////////////////////// @@ -163,17 +162,13 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt if (block.getHeight() == heightForRepublishing) { // We only republish if we are completed with parsing old blocks, otherwise we would republish old // proposals all the time - if (parsingComplete) { - publishToAppendOnlyDataStore(); - fillListFromAppendOnlyDataStore(); - } + publishToAppendOnlyDataStore(); + fillListFromAppendOnlyDataStore(); } } @Override public void onParseBlockChainComplete() { - parsingComplete = true; - // Fill the lists with the data we have collected in out stores. fillListFromProtectedStore(); fillListFromAppendOnlyDataStore(); @@ -181,10 +176,9 @@ public class ProposalService implements HashMapChangedListener, AppendOnlyDataSt /////////////////////////////////////////////////////////////////////////////////////////// - // Getter + // API /////////////////////////////////////////////////////////////////////////////////////////// - public List getValidatedProposals() { return proposalPayloads.stream() .map(ProposalPayload::getProposal) diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java index d0b47df820..24d38f03d2 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/MissingDataRequestService.java @@ -18,25 +18,41 @@ package bisq.core.dao.governance.voteresult; import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.BlindVoteListService; import bisq.core.dao.governance.blindvote.network.RepublishGovernanceDataHandler; +import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; + +import bisq.network.p2p.P2PService; + +import bisq.common.UserThread; import javax.inject.Inject; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import lombok.Getter; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class MissingDataRequestService implements DaoSetupService { private final RepublishGovernanceDataHandler republishGovernanceDataHandler; - - @Getter - private final ObservableList voteResultExceptions = FXCollections.observableArrayList(); + private final BlindVoteListService blindVoteListService; + private final ProposalService proposalService; + private final P2PService p2PService; @Inject - public MissingDataRequestService(RepublishGovernanceDataHandler republishGovernanceDataHandler) { + public MissingDataRequestService(RepublishGovernanceDataHandler republishGovernanceDataHandler, + BlindVoteListService blindVoteListService, + ProposalService proposalService, + P2PService p2PService) { this.republishGovernanceDataHandler = republishGovernanceDataHandler; + this.blindVoteListService = blindVoteListService; + this.proposalService = proposalService; + this.p2PService = p2PService; } @@ -46,12 +62,6 @@ public class MissingDataRequestService implements DaoSetupService { @Override public void addListeners() { - voteResultExceptions.addListener((ListChangeListener) c -> { - c.next(); - if (c.wasAdded()) { - republishGovernanceDataHandler.sendRepublishRequest(); - } - }); } @Override @@ -63,7 +73,45 @@ public class MissingDataRequestService implements DaoSetupService { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void addVoteResultException(VoteResultException voteResultException) { - this.voteResultExceptions.add(voteResultException); + public void sendRepublishRequest() { + republishGovernanceDataHandler.sendRepublishRequest(); + } + + public void reRepublishAllGovernanceData() { + log.warn("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " + + "blindVotePayloads to the P2P network."); + + ObservableList proposalPayloads = proposalService.getProposalPayloads(); + proposalPayloads.forEach(proposalPayload -> { + // We want a random delay between 0.1 and 30 sec. depending on the number of items + int delay = Math.max(100, Math.min(30_000, new Random().nextInt(proposalPayloads.size() * 500))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); + String txId = proposalPayload.getProposal().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a proposalPayload to " + + "the P2P network as append only data. proposalTxId={}", txId); + } else { + log.error("Adding of proposalPayload to P2P network failed. proposalTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); + + ObservableList blindVotePayloads = blindVoteListService.getBlindVotePayloads(); + blindVotePayloads + .forEach(blindVotePayload -> { + // We want a random delay between 0.1 and 30 sec. depending on the number of items + int delay = Math.max(100, Math.min(30_000, new Random().nextInt(blindVotePayloads.size() * 500))); + UserThread.runAfter(() -> { + boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); + String txId = blindVotePayload.getBlindVote().getTxId(); + if (success) { + log.debug("We received a RepublishGovernanceDataRequest and re-published a blindVotePayload to " + + "the P2P network as append only data. blindVoteTxId={}", txId); + } else { + log.error("Adding of blindVotePayload to P2P network failed. blindVoteTxId={}", txId); + } + }, delay, TimeUnit.MILLISECONDS); + }); } } diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java index a69b8d98e9..15c50a1b09 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultConsensus.java @@ -68,27 +68,31 @@ public class VoteResultConsensus { // hex encoded hashOfProposalList for comparision @Nullable public static byte[] getMajorityHash(List hashWithStakeList) - throws VoteResultException.ConsensusException, VoteResultException.ValidationException { + throws VoteResultException.ValidationException, VoteResultException.ConsensusException { try { checkArgument(!hashWithStakeList.isEmpty(), "hashWithStakeList must not be empty"); - hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed() - .thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash()))); - - // If there are conflicting data views (multiple hashes) we only consider the voting round as valid if - // the majority is a super majority with > 80%. - if (hashWithStakeList.size() > 1) { - long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum(); - long stakeOfFirst = hashWithStakeList.get(0).getStake(); - if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) { - throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " + - "total stake of all data views. We consider the voting cycle as invalid if the " + - "winning data view does not reach a super majority."); - } - } - return hashWithStakeList.get(0).getHash(); } catch (Throwable t) { throw new VoteResultException.ValidationException(t); } + + hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed() + .thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash()))); + + // If there are conflicting data views (multiple hashes) we only consider the voting round as valid if + // the majority is a super majority with > 80%. + if (hashWithStakeList.size() > 1) { + long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum(); + long stakeOfFirst = hashWithStakeList.get(0).getStake(); + if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) { + log.warn("The winning data view has less then 80% of the " + + "total stake of all data views. We consider the voting cycle as invalid if the " + + "winning data view does not reach a super majority. hashWithStakeList={}", hashWithStakeList); + throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " + + "total stake of all data views. We consider the voting cycle as invalid if the " + + "winning data view does not reach a super majority."); + } + } + return hashWithStakeList.get(0).getHash(); } // Key is stored after version and type bytes and list of Blind votes. It has 16 bytes diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java index 7e2814b3de..e6f338c1bd 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultException.java @@ -18,34 +18,37 @@ package bisq.core.dao.governance.voteresult; import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.Cycle; import java.util.List; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.Value; public class VoteResultException extends Exception { + @Getter + private final int heightOfFirstBlockInCycle; - VoteResultException(Throwable cause) { + VoteResultException(Cycle cycle, Throwable cause) { super(cause); - } - - private VoteResultException(String message) { - super(message); - } - - private VoteResultException(String message, Throwable cause) { - super(message, cause); + this.heightOfFirstBlockInCycle = cycle.getHeightOfFirstBlock(); } @Override public String toString() { return "VoteResultException{" + + "\n heightOfFirstBlockInCycle=" + heightOfFirstBlockInCycle + "\n} " + super.toString(); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static sub classes + /////////////////////////////////////////////////////////////////////////////////////////// + @EqualsAndHashCode(callSuper = true) - public static class ConsensusException extends VoteResultException { + public static class ConsensusException extends Exception { ConsensusException(String message) { super(message); @@ -59,7 +62,7 @@ public class VoteResultException extends Exception { } @EqualsAndHashCode(callSuper = true) - public static class ValidationException extends VoteResultException { + public static class ValidationException extends Exception { ValidationException(Throwable cause) { super("Validation of vote result failed.", cause); @@ -74,7 +77,7 @@ public class VoteResultException extends Exception { } @EqualsAndHashCode(callSuper = true) - public static abstract class MissingDataException extends VoteResultException { + public static abstract class MissingDataException extends Exception { private MissingDataException(String message) { super(message); } @@ -114,7 +117,7 @@ public class VoteResultException extends Exception { @EqualsAndHashCode(callSuper = true) @Value - public static class DecryptionException extends VoteResultException { + public static class DecryptionException extends Exception { public DecryptionException(Throwable cause) { super(cause); } diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index c0c8eea28b..309199dad1 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -33,12 +33,14 @@ import bisq.core.dao.governance.votereveal.VoteRevealConsensus; import bisq.core.dao.governance.votereveal.VoteRevealService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.dao.state.model.governance.Ballot; import bisq.core.dao.state.model.governance.BallotList; import bisq.core.dao.state.model.governance.ChangeParamProposal; import bisq.core.dao.state.model.governance.ConfiscateBondProposal; +import bisq.core.dao.state.model.governance.Cycle; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; import bisq.core.dao.state.model.governance.EvaluatedProposal; @@ -55,6 +57,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.network.p2p.storage.P2PDataStorage; import bisq.common.util.MathUtils; +import bisq.common.util.PermutationUtil; import bisq.common.util.Utilities; import javax.inject.Inject; @@ -140,7 +143,6 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { @Override public void start() { - maybeCalculateVoteResult(daoStateService.getChainHeight()); } @@ -150,14 +152,17 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { @Override public void onNewBlockHeight(int blockHeight) { - // TODO check if we should use onParseTxsComplete for calling maybeCalculateVoteResult - maybeCalculateVoteResult(blockHeight); } @Override public void onParseBlockChainComplete() { } + @Override + public void onParseTxsComplete(Block block) { + maybeCalculateVoteResult(block.getHeight()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -165,6 +170,7 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { private void maybeCalculateVoteResult(int chainHeight) { if (isInVoteResultPhase(chainHeight)) { + Cycle currentCycle = periodService.getCurrentCycle(); long startTs = System.currentTimeMillis(); Set decryptedBallotsWithMeritsSet = getDecryptedBallotsWithMeritsSet(chainHeight); decryptedBallotsWithMeritsSet.stream() @@ -207,19 +213,19 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { } } catch (VoteResultException.ValidationException e) { - log.error(e.toString()); + log.warn(e.toString()); e.printStackTrace(); - voteResultExceptions.add(e); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); } catch (VoteResultException.ConsensusException e) { - log.error(e.toString()); - log.error("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); + log.warn(e.toString()); + log.warn("decryptedBallotsWithMeritsSet " + decryptedBallotsWithMeritsSet); e.printStackTrace(); //TODO notify application of that case (e.g. add error handler) // The vote cycle is invalid as conflicting data views of the blind vote data exist and the winner // did not reach super majority of 80%. - voteResultExceptions.add(e); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); } } else { log.info("There have not been any votes in that cycle. chainHeight={}", chainHeight); @@ -256,6 +262,7 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { return null; } + Cycle currentCycle = periodService.getCurrentCycle(); try { // TODO maybe verify version in opReturn @@ -291,12 +298,12 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { return new DecryptedBallotsWithMerits(hashOfBlindVoteList, blindVoteTxId, voteRevealTxId, blindVoteStake, ballotList, meritList); } catch (VoteResultException.MissingBallotException missingBallotException) { log.warn("We are missing proposals to create the vote result: " + missingBallotException.toString()); - missingDataRequestService.addVoteResultException(missingBallotException); - voteResultExceptions.add(missingBallotException); + missingDataRequestService.sendRepublishRequest(); + voteResultExceptions.add(new VoteResultException(currentCycle, missingBallotException)); return null; } catch (VoteResultException.DecryptionException decryptionException) { - log.error("Could not decrypt data: " + decryptionException.toString()); - voteResultExceptions.add(decryptionException); + log.warn("Could not decrypt data: " + decryptionException.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, decryptionException)); return null; } } else { @@ -306,17 +313,17 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { "recover the missing blind vote by a request to our peers. blindVoteTxId={}", blindVoteTxId); VoteResultException.MissingBlindVoteDataException voteResultException = new VoteResultException.MissingBlindVoteDataException(blindVoteTxId); - missingDataRequestService.addVoteResultException(voteResultException); - voteResultExceptions.add(voteResultException); + missingDataRequestService.sendRepublishRequest(); + voteResultExceptions.add(new VoteResultException(currentCycle, voteResultException)); return null; } } catch (VoteResultException.ValidationException e) { - log.error("Could not create DecryptedBallotsWithMerits because of voteResultValidationException: " + e.toString()); - voteResultExceptions.add(e); + log.warn("Could not create DecryptedBallotsWithMerits because of voteResultValidationException: " + e.toString()); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); return null; } catch (Throwable e) { log.error("Could not create DecryptedBallotsWithMerits because of an unknown exception: " + e.toString()); - voteResultExceptions.add(new VoteResultException(e)); + voteResultExceptions.add(new VoteResultException(currentCycle, e)); return null; } }) @@ -414,28 +421,38 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { // It still could be that we have additional blind votes so our hash does not match. We can try to permute // our list with excluding items to see if we get a matching list. If not last resort is to request the // missing items from the network. - List permutatedListMatchingMajority = findPermutatedListMatchingMajority(majorityVoteListHash); - if (!permutatedListMatchingMajority.isEmpty()) { - log.info("We found a permutation of our blindVote list which matches the majority view. " + - "permutatedListMatchingMajority={}", permutatedListMatchingMajority); + Optional> permutatedList = findPermutatedListMatchingMajority(majorityVoteListHash); + if (permutatedList.isPresent()) { //TODO do we need to apply/store it for later use? + + return true; } else { - log.info("We did not find a permutation of our blindVote list which matches the majority view. " + + log.warn("We did not find a permutation of our blindVote list which matches the majority view. " + "We will request the blindVote data from the peers."); // This is async operation. We will restart the whole verification process once we received the data. - requestBlindVoteListFromNetwork(majorityVoteListHash); + missingDataRequestService.sendRepublishRequest(); } } return matches; } - private List findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { + private Optional> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - while (!list.isEmpty() && !isListMatchingMajority(majorityVoteListHash, list)) { - // We remove first item as it will be sorted anyway... - list.remove(0); + long ts = System.currentTimeMillis(); + List> result = PermutationUtil.findAllPermutations(list, 1000000); + for (List variation : result) { + if (isListMatchingMajority(majorityVoteListHash, variation)) { + log.info("We found a variation of the blind vote list which matches the majority hash. variation={}", + variation); + log.info("findPermutatedListMatchingMajority for {} items took {} ms.", + list.size(), (System.currentTimeMillis() - ts)); + return Optional.of(variation); + } } - return list; + log.info("We did not find a variation of the blind vote list which matches the majority hash."); + log.info("findPermutatedListMatchingMajority for {} items took {} ms.", + list.size(), (System.currentTimeMillis() - ts)); + return Optional.empty(); } private boolean isListMatchingMajority(byte[] majorityVoteListHash, List list) { @@ -443,10 +460,6 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { return Arrays.equals(majorityVoteListHash, hashOfBlindVoteList); } - private void requestBlindVoteListFromNetwork(byte[] majorityVoteListHash) { - //TODO impl - } - private Set getEvaluatedProposals(Set decryptedBallotsWithMeritsSet, int chainHeight) { // We reorganize the data structure to have a map of proposals with a list of VoteWithStake objects Map> resultListByProposalMap = getVoteWithStakeListByProposalMap(decryptedBallotsWithMeritsSet); diff --git a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java index 5a2aea27c5..fd16b89859 100644 --- a/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java +++ b/core/src/main/java/bisq/core/dao/governance/votereveal/VoteRevealService.java @@ -36,6 +36,7 @@ import bisq.core.dao.node.BsqNode; import bisq.core.dao.node.BsqNodeProvider; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.dao.state.model.governance.DaoPhase; @@ -54,6 +55,7 @@ import javafx.collections.ObservableList; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -73,6 +75,11 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j public class VoteRevealService implements DaoStateListener, DaoSetupService { + + public interface VoteRevealTxPublishedListener { + void onVoteRevealTxPublished(String txId); + } + private final DaoStateService daoStateService; private final BlindVoteListService blindVoteListService; private final PeriodService periodService; @@ -86,7 +93,7 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { @Getter private final ObservableList voteRevealExceptions = FXCollections.observableArrayList(); private final BsqNode bsqNode; - + private final List voteRevealTxPublishedListeners = new ArrayList<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -131,7 +138,6 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { @Override public void start() { - maybeRevealVotes(); } @@ -141,7 +147,14 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { public byte[] getHashOfBlindVoteList() { List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - return VoteRevealConsensus.getHashOfBlindVoteList(blindVotes); + byte[] hashOfBlindVoteList = VoteRevealConsensus.getHashOfBlindVoteList(blindVotes); + log.info("blindVoteList for creating hash: " + blindVotes); + log.info("Sha256Ripemd160 hash of hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList)); + return hashOfBlindVoteList; + } + + public void addVoteRevealTxPublishedListener(VoteRevealTxPublishedListener voteRevealTxPublishedListener) { + voteRevealTxPublishedListeners.add(voteRevealTxPublishedListener); } @@ -151,40 +164,43 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { @Override public void onNewBlockHeight(int blockHeight) { - // TODO check if we should use onParseTxsComplete for calling maybeCalculateVoteResult - - maybeRevealVotes(); } @Override public void onParseBlockChainComplete() { } + @Override + public void onParseTxsCompleteAfterBatchProcessing(Block block) { + maybeRevealVotes(block.getHeight()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// // Creation of vote reveal tx is done without user activity! - // We create automatically the vote reveal tx when we enter the reveal phase of the current cycle when + // We create automatically the vote reveal tx when we are in the reveal phase of the current cycle when // the blind vote was created in case we have not done it already. // The voter need to be at least once online in the reveal phase when he has a blind vote created, - // otherwise his vote becomes invalid and his locked stake will get unlocked - private void maybeRevealVotes() { - // We must not use daoStateService.getChainHeight() because that gets updated with each parsed block but we - // only want to publish the vote reveal tx if our current real chain height is matching the cycle and phase and - // not at any intermediate height during parsing all blocks. The bsqNode knows the latest height from either - // Bitcoin Core or from the seed node. - int chainHeight = bsqNode.getChainTipHeight(); + // otherwise his vote becomes invalid. + // In case the user miss the vote reveal phase an (invalid) vote reveal tx will be created the next time the user is + // online. That tx only serves the purpose to unlock the stake from the blind vote but it will be ignored for voting. + // A blind vote which did not get revealed might still be part of the majority hash calculation as we cannot know + // which blind votes might be revealed until the phase is over at the moment when we publish the vote reveal tx. + private void maybeRevealVotes(int chainHeight) { myVoteListService.getMyVoteList().stream() .filter(myVote -> myVote.getRevealTxId() == null) // we have not already revealed .forEach(myVote -> { boolean isInVoteRevealPhase = periodService.getPhaseForHeight(chainHeight) == DaoPhase.Phase.VOTE_REVEAL; boolean isBlindVoteTxInCorrectPhaseAndCycle = periodService.isTxInPhaseAndCycle(myVote.getTxId(), DaoPhase.Phase.BLIND_VOTE, chainHeight); if (isInVoteRevealPhase && isBlindVoteTxInCorrectPhaseAndCycle) { + log.info("We call revealVote at blockHeight {} for blindVoteTxId {}", chainHeight, myVote.getTxId()); // Standard case that we are in the correct phase and cycle and create the reveal tx. - revealVote(myVote); + revealVote(myVote, true); } else { + // We missed the vote reveal phase but publish a vote reveal tx to unlock the blind vote stake. boolean isAfterVoteRevealPhase = periodService.getPhaseForHeight(chainHeight).ordinal() > DaoPhase.Phase.VOTE_REVEAL.ordinal(); // We missed the reveal phase but we are in the correct cycle @@ -203,32 +219,30 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { // As this is an exceptional case we prefer to have a simple solution instead and just // publish the vote reveal tx but are aware that is is invalid. log.warn("We missed the vote reveal phase but publish now the tx to unlock our locked " + - "BSQ from the blind vote tx. BlindVoteTxId={}", myVote.getTxId()); + "BSQ from the blind vote tx. BlindVoteTxId={}, blockHeight={}", + myVote.getTxId(), chainHeight); // We handle the exception here inside the stream iteration as we have not get triggered from an // outside user intent anyway. We keep errors in a observable list so clients can observe that to // get notified if anything went wrong. - revealVote(myVote); + revealVote(myVote, false); } } }); } - private void revealVote(MyVote myVote) { + private void revealVote(MyVote myVote, boolean inBlindVotePhase) { try { // We collect all valid blind vote items we received via the p2p network. // It might be that different nodes have a different collection of those items. // To ensure we get a consensus of the data for later calculating the result we will put a hash of each - // voters blind vote collection into the opReturn data and check for a majority at issuance time. + // voter's blind vote collection into the opReturn data and check for a majority in the vote result phase. // The voters "vote" with their stake at the reveal tx for their version of the blind vote collection. - // TODO make more clear by using param like here: - /* List blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - VoteRevealConsensus.getHashOfBlindVoteList(blindVotes);*/ - - byte[] hashOfBlindVoteList = getHashOfBlindVoteList(); - - log.info("Sha256Ripemd160 hash of hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList)); + // If we are not in the right phase we just add an empty hash (still need to have the hash as otherwise we + // would not recognize the tx as vote reveal tx) + byte[] hashOfBlindVoteList = inBlindVotePhase ? getHashOfBlindVoteList() : new byte[20]; + log.info("revealVote: Sha256Ripemd160 hash of hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList)); byte[] opReturnData = VoteRevealConsensus.getOpReturnData(hashOfBlindVoteList, myVote.getSecretKey()); // We search for my unspent stake output. @@ -245,14 +259,15 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { log.info("voteRevealTx={}", voteRevealTx); publishTx(voteRevealTx); - // TODO add comment... // We don't want to wait for a successful broadcast to avoid issues if the broadcast succeeds delayed or at // next startup but the tx was actually broadcasted. myVoteListService.applyRevealTxId(myVote, voteRevealTx.getHashAsString()); - // Just for additional resilience we republish our blind votes - List sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); - rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle); + if (inBlindVotePhase) { + // Just for additional resilience we republish our blind votes + List sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); + rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle); + } } catch (IOException | WalletException | TransactionVerificationException | InsufficientMoneyException e) { voteRevealExceptions.add(new VoteRevealException("Exception at calling revealVote.", @@ -267,6 +282,7 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService { @Override public void onSuccess(Transaction transaction) { log.info("voteRevealTx successfully broadcasted."); + voteRevealTxPublishedListeners.forEach(l -> l.onVoteRevealTxPublished(transaction.getHashAsString())); } @Override diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java index 160ec4438b..4ac7980921 100644 --- a/core/src/main/java/bisq/core/dao/node/BsqNode.java +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -63,7 +63,11 @@ public abstract class BsqNode implements DaoSetupService { @Nullable protected Consumer warnMessageHandler; protected List pendingBlocks = new ArrayList<>(); + // The chain height of the latest Block we either get reported by Bitcoin Core or from the seed node + // This property should not be used in consensus code but only for retrieving blocks as it is not in sync with the + // parsing and the daoState. It also does not represent the latest blockHeight but the currently received + // (not parsed) block. @Getter protected int chainTipHeight; diff --git a/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java index 64afc87751..1a6618ca8f 100644 --- a/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java +++ b/core/src/main/java/bisq/core/dao/node/full/network/FullNodeNetworkService.java @@ -17,18 +17,14 @@ package bisq.core.dao.node.full.network; -import bisq.core.dao.governance.blindvote.BlindVoteListService; import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; -import bisq.core.dao.governance.blindvote.storage.BlindVotePayload; -import bisq.core.dao.governance.proposal.ProposalService; -import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; import bisq.core.dao.node.full.RawBlock; import bisq.core.dao.node.messages.GetBlocksRequest; import bisq.core.dao.node.messages.NewBlockBroadcastMessage; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; -import bisq.network.p2p.P2PService; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.MessageListener; import bisq.network.p2p.network.NetworkNode; @@ -41,12 +37,8 @@ import bisq.common.proto.network.NetworkEnvelope; import javax.inject.Inject; -import javafx.collections.ObservableList; - import java.util.HashMap; import java.util.Map; -import java.util.Random; -import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @@ -68,9 +60,7 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List private final NetworkNode networkNode; private final PeerManager peerManager; private final Broadcaster broadcaster; - private final BlindVoteListService blindVoteListService; - private final ProposalService proposalService; - private final P2PService p2PService; + private final MissingDataRequestService missingDataRequestService; private final DaoStateService daoStateService; // Key is connection UID @@ -86,16 +76,12 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List public FullNodeNetworkService(NetworkNode networkNode, PeerManager peerManager, Broadcaster broadcaster, - BlindVoteListService blindVoteListService, - ProposalService proposalService, - P2PService p2PService, + MissingDataRequestService missingDataRequestService, DaoStateService daoStateService) { this.networkNode = networkNode; this.peerManager = peerManager; this.broadcaster = broadcaster; - this.blindVoteListService = blindVoteListService; - this.proposalService = proposalService; - this.p2PService = p2PService; + this.missingDataRequestService = missingDataRequestService; this.daoStateService = daoStateService; } @@ -195,38 +181,7 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List log.warn("We have stopped already. We ignore that onMessage call."); } } else if (networkEnvelope instanceof RepublishGovernanceDataRequest) { - ObservableList blindVotePayloads = blindVoteListService.getBlindVotePayloads(); - blindVotePayloads - .forEach(blindVotePayload -> { - // We want a random delay between 0.1 and 30 sec. depending on the number of items - int delay = Math.max(100, Math.min(30_000, new Random().nextInt(blindVotePayloads.size() * 500))); - UserThread.runAfter(() -> { - boolean success = p2PService.addPersistableNetworkPayload(blindVotePayload, true); - String txId = blindVotePayload.getBlindVote().getTxId(); - if (success) { - log.warn("We received a RepublishGovernanceDataRequest and re-published a blindVotePayload to " + - "the P2P network as append only data. blindVoteTxId={}", txId); - } else { - log.error("Adding of blindVotePayload to P2P network failed. blindVoteTxId={}", txId); - } - }, delay, TimeUnit.MILLISECONDS); - }); - - ObservableList proposalPayloads = proposalService.getProposalPayloads(); - proposalPayloads.forEach(proposalPayload -> { - // We want a random delay between 0.1 and 30 sec. depending on the number of items - int delay = Math.max(100, Math.min(30_000, new Random().nextInt(proposalPayloads.size() * 500))); - UserThread.runAfter(() -> { - boolean success = p2PService.addPersistableNetworkPayload(proposalPayload, true); - String txId = proposalPayload.getProposal().getTxId(); - if (success) { - log.warn("We received a RepublishGovernanceDataRequest and re-published a proposalPayload to " + - "the P2P network as append only data. proposalTxId={}", txId); - } else { - log.error("Adding of proposalPayload to P2P network failed. proposalTxId={}", txId); - } - }, delay, TimeUnit.MILLISECONDS); - }); + missingDataRequestService.reRepublishAllGovernanceData(); } } } diff --git a/core/src/main/java/bisq/core/util/BsqFormatter.java b/core/src/main/java/bisq/core/util/BsqFormatter.java index f1e671db1f..92a91b3cf5 100644 --- a/core/src/main/java/bisq/core/util/BsqFormatter.java +++ b/core/src/main/java/bisq/core/util/BsqFormatter.java @@ -20,6 +20,7 @@ package bisq.core.util; import bisq.core.app.BisqEnvironment; import bisq.core.dao.exceptions.ValidationException; import bisq.core.dao.governance.param.Param; +import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.provider.price.MarketPrice; import bisq.core.util.validation.BtcAddressValidator; @@ -36,6 +37,9 @@ import org.bitcoinj.utils.MonetaryFormat; import javax.inject.Inject; import java.text.DecimalFormat; +import java.text.NumberFormat; + +import java.util.Locale; import lombok.extern.slf4j.Slf4j; @@ -44,14 +48,17 @@ public class BsqFormatter extends BSFormatter { @SuppressWarnings("PointlessBooleanExpression") private static final boolean useBsqAddressFormat = true || !DevEnv.isDevMode(); private final String prefix = "B"; - private final DecimalFormat amountFormat = new DecimalFormat("###,###,###.##"); - private final DecimalFormat marketCapFormat = new DecimalFormat("###,###,###"); + private DecimalFormat amountFormat; + private DecimalFormat marketCapFormat; private final MonetaryFormat btcCoinFormat; @Inject public BsqFormatter() { super(); + GlobalSettings.localeProperty().addListener((observable, oldValue, newValue) -> setFormatter(newValue)); + setFormatter(GlobalSettings.getLocale()); + btcCoinFormat = super.coinFormat; final String baseCurrencyCode = BisqEnvironment.getBaseCurrencyNetwork().getCurrencyCode(); @@ -73,6 +80,16 @@ public class BsqFormatter extends BSFormatter { amountFormat.setMinimumFractionDigits(2); } + private void setFormatter(Locale locale) { + amountFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); + amountFormat.setMinimumFractionDigits(2); + amountFormat.setMaximumFractionDigits(2); + + marketCapFormat = (DecimalFormat) NumberFormat.getNumberInstance(locale); + marketCapFormat = new DecimalFormat(); + marketCapFormat.setMaximumFractionDigits(0); + } + /** * Returns the base-58 encoded String representation of this * object, including version and checksum bytes. diff --git a/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java b/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java new file mode 100644 index 0000000000..6b088eebf9 --- /dev/null +++ b/core/src/main/java/bisq/core/util/validation/UrlInputValidator.java @@ -0,0 +1,45 @@ +/* + * 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.core.util.validation; + +import bisq.core.locale.Res; + +import java.net.URL; + +import static com.google.common.base.Preconditions.checkArgument; + +public class UrlInputValidator extends InputValidator { + + public UrlInputValidator() { + } + + public ValidationResult validate(String input) { + ValidationResult validationResult = super.validate(input); + if (!validationResult.isValid) + return validationResult; + + try { + new URL(input); // does not cover all invalid urls, so we use a regex as well + String regex = "^(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"; + checkArgument(input.matches(regex), "URL does not match regex"); + return validationResult; + } catch (Throwable t) { + return new ValidationResult(false, Res.get("validation.invalidUrl")); + } + } +} diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index aef07d49dd..1e4e78aa57 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1204,6 +1204,10 @@ dao.cycle.voteReveal=Vote reveal phase dao.cycle.voteResult=Vote result dao.cycle.phaseDuration={0} blocks (≈{1}); Block {2} - {3} (≈{4} - ≈{5}) +dao.voteReveal.txPublished.headLine=Vote reveal transaction published +dao.voteReveal.txPublished=Your vote reveal transaction with transaction ID {0} was successfully published.\n\n\ + This happens automatically by the software if you have participated in the DAO voting. + dao.results.cycles.header=Cycles dao.results.cycles.table.header.cycle=Cycle dao.results.cycles.table.header.numProposals=Proposals @@ -1221,6 +1225,7 @@ dao.results.proposals.table.header.result=Vote result dao.results.proposals.voting.detail.header=Vote results for selected proposal +dao.results.exceptions=Vote result exception(s) # suppress inspection "UnusedProperty" dao.param.UNDEFINED=Undefined @@ -1438,7 +1443,7 @@ dao.bond.bondedRoleType.MEDIATOR=Mediator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.ARBITRATOR=Arbitrator -dao.burnBsq.assetFee=Asset listing fee +dao.burnBsq.assetFee=Asset listing dao.burnBsq.menuItem.assetFee=Asset listing fee dao.burnBsq.menuItem.proofOfBurn=Proof of burn dao.burnBsq.header=Fee for asset listing @@ -1594,6 +1599,7 @@ dao.proposal.display.myVote.accepted=Accepted dao.proposal.display.myVote.rejected=Rejected dao.proposal.display.myVote.ignored=Ignored dao.proposal.myVote.summary=Voted: {0}; Vote weight: {1} (earned: {2} + stake: {3}); +dao.proposal.myVote.invalid=Vote was invalid dao.proposal.voteResult.success=Accepted dao.proposal.voteResult.failed=Rejected @@ -1608,7 +1614,9 @@ dao.proposal.display.assetComboBox.label=Asset to remove dao.blindVote=blind vote dao.blindVote.startPublishing=Publishing blind vote transaction... -dao.blindVote.success=Your blind vote has been successfully published. +dao.blindVote.success=Your blind vote transaction has been successfully published.\n\nPlease note, that you have to be \ + online in the vote reveal phase so that your Bisq application can publish the vote reveal transaction. \ + Without the vote reveal transaction your vote would be invalid! dao.wallet.menuItem.send=Send dao.wallet.menuItem.receive=Receive @@ -1639,9 +1647,20 @@ dao.wallet.dashboard.burntTx=No. of all fee payments transactions dao.wallet.dashboard.price=Latest BSQ/BTC trade price (in Bisq) dao.wallet.dashboard.marketCap=Market capitalisation (based on trade price) -dao.wallet.receive.fundYourWallet=Fund your BSQ wallet +dao.wallet.receive.fundYourWallet=Your BSQ receive address dao.wallet.receive.bsqAddress=BSQ wallet address +dao.wallet.receive.dao.headline=The Bisq DAO +dao.wallet.receive.daoInfo=Just as the Bisq exchange is decentralized and censorship-resistant, so is its governance \ + model — and the Bisq DAO and BSQ token are the tools that make it possible. +dao.wallet.receive.daoInfo.button=Learn more about the Bisq DAO +dao.wallet.receive.daoTestnetInfo=The mainnet Bisq DAO is not launched yet but you can learn about the Bisq DAO by \ + running it on testnet. +dao.wallet.receive.daoTestnetInfo.button=How to run the Bisq DAO on testnet +dao.wallet.receive.daoContributorInfo=If you have contributed to Bisq please use the \ + BSQ address below and make a request for taking part of the BSQ genesis distribution. +dao.wallet.receive.daoContributorInfo.button=How to be part of the BSQ genesis distribution + dao.wallet.send.sendFunds=Send funds dao.wallet.send.sendBtcFunds=Send non-BSQ funds (BTC) dao.wallet.send.amount=Amount in BSQ @@ -1700,7 +1719,12 @@ dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\n\ Issuance date: {0} -dao.proposal.create.missingFunds=You don''t have sufficient funds for creating the proposal.\n\ +dao.proposal.create.missingBsqFunds=You don''t have sufficient BSQ funds for creating the proposal. If you have an \ + unconfirmed BSQ transaction you need to wait for a blockchain confirmation because BSQ is validated only if it is \ + included in a block.\n\ + Missing: {0} +dao.proposal.create.missingMinerFeeFunds=You don''t have sufficient BTC funds for creating the proposal transaction. \ + Any BSQ transaction require also a miner fee in BTC.\n\ Missing: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\n\ @@ -2566,3 +2590,4 @@ validation.length=Length must be between {0} and {1} validation.pattern=Input must be of format: {0} validation.noHexString=The input is not in HEX format. validation.advancedCash.invalidFormat=Must be a valid email or wallet id of format: X000000000000 +validation.invalidUrl=This is not a valid URL diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index c757dd4f91..018b607114 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Entlohnungsanfrage/ausgabe dao.tx.issuanceFromCompReq.tooltip=Entlohnungsanfrage, die zur Ausgabe neuere BSQ führte.\nAusgabedatum: {0} dao.tx.issuanceFromReimbursement=Rückerstattungsantrag/Ausgabe dao.tx.issuanceFromReimbursement.tooltip=Rückerstattungsanfrage, die zur Ausgabe neuer BSQ führte.\nAusgabedatum: {0} -dao.proposal.create.missingFunds=Sie haben nicht genügend Gelder um den Vorschlag zu erstellen.\nFehlend: {0} +dao.proposal.create.missingBsqFunds=Sie haben nicht genügend Gelder um den Vorschlag zu erstellen.\nFehlend: {0} dao.feeTx.confirm=Bestätige {0} Transaktion dao.feeTx.confirm.details={0} Gebühr: {1}\nMining-Gebühr: {2} ({3} Satoshis/Byte)\nTransaktionsgröße: {4} Kb\n\nSind Sie sicher, dass Sie die {5} Transaktion senden wollen? diff --git a/core/src/main/resources/i18n/displayStrings_el.properties b/core/src/main/resources/i18n/displayStrings_el.properties index 0201fe7aad..791fb41f63 100644 --- a/core/src/main/resources/i18n/displayStrings_el.properties +++ b/core/src/main/resources/i18n/displayStrings_el.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Αίτημα/έκδοση αποζημίωσης dao.tx.issuanceFromCompReq.tooltip=Αίτημα αποζημίωσης το οποίο οδήγησε σε έκδοση νέων BSQ.\nΗμερομηνία έκδοσης: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=Δεν έχεις επαρκή κεφάλαια για τη δημιουργία της πρότασης.\nΥπολείπονται: {0} +dao.proposal.create.missingBsqFunds=Δεν έχεις επαρκή κεφάλαια για τη δημιουργία της πρότασης.\nΥπολείπονται: {0} dao.feeTx.confirm=Επιβεβαίωση συναλλαγής {0} dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index b7832a6325..9585f8567d 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Solicitud/emisión de compensación dao.tx.issuanceFromCompReq.tooltip=Solicitud de compensación que lleva a emitir nuevos BSQ.\nFecha de emisión: {0} dao.tx.issuanceFromReimbursement=Solicitud de reembolso/emisión dao.tx.issuanceFromReimbursement.tooltip=Solicitud de reembolso que lleva a una emisión de nuevos BSQ.\nFecha de emisión: {0} -dao.proposal.create.missingFunds=No tiene suficientes fondos para crear la propuesta.\nFaltan: {0} +dao.proposal.create.missingBsqFunds=No tiene suficientes fondos para crear la propuesta.\nFaltan: {0} dao.feeTx.confirm=Confirmar transacción {0} dao.feeTx.confirm.details={0} tasa: {1}\nTasa de minado: {2} ({3} Satoshis/byte)\nTamaño de la transacción: {4} Kb\n\nEstá seguro de que quire publicar la transacción {5}? diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 2eb7b028bd..672af9be47 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=درخواست/صدور خسارت dao.tx.issuanceFromCompReq.tooltip=درخواست خسارت که منجر به صدور BSQ جدید می‌شود.\nتاریخ صدور: {0} dao.tx.issuanceFromReimbursement=درخواست/صدور بازپرداخت dao.tx.issuanceFromReimbursement.tooltip=درخواست بازپرداختی که منجر به صدور BSQ جدید می‌شود.\nتاریخ صدور: {0} -dao.proposal.create.missingFunds=شما وجوه کافی برای ایجاد پیشنهاد را ندارید.\nمقدار مورد نیاز: {0} +dao.proposal.create.missingBsqFunds=شما وجوه کافی برای ایجاد پیشنهاد را ندارید.\nمقدار مورد نیاز: {0} dao.feeTx.confirm=تایید {0} تراکنش dao.feeTx.confirm.details=کارمزد {0}: {1}\nکارمزد استخراج: {2} ({3} ساتوشی بر بایت)\nاندازه تراکنش: {4} Kb\n\nآیا از انتشار تراکنش {5} اطمینان دارید؟ diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 5baa7938f9..6ec7a5220e 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\nIssuance date: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} +dao.proposal.create.missingBsqFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/core/src/main/resources/i18n/displayStrings_hu.properties b/core/src/main/resources/i18n/displayStrings_hu.properties index 3b232ffb5a..037e9db8af 100644 --- a/core/src/main/resources/i18n/displayStrings_hu.properties +++ b/core/src/main/resources/i18n/displayStrings_hu.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\nIssuance date: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=Nem rendelkezik elegendő összegekkel a kártérítési kérelem létrehozásához.\nHiányzó: {0} +dao.proposal.create.missingBsqFunds=Nem rendelkezik elegendő összegekkel a kártérítési kérelem létrehozásához.\nHiányzó: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 63b928d4f8..2f2fc18169 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\nIssuance date: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=Você não tem saldo suficiente para criar a proposta.\nFaltam: {0} +dao.proposal.create.missingBsqFunds=Você não tem saldo suficiente para criar a proposta.\nFaltam: {0} dao.feeTx.confirm=Confirmar transação {0} dao.feeTx.confirm.details=Taxa de {0}: {1}\nTaxa de mineração: {2} ({3} satoshis/byte)\nTamanho da transação: {4} Kb\n\nTem certeza de que deseja publicar a transação {5}? diff --git a/core/src/main/resources/i18n/displayStrings_ro.properties b/core/src/main/resources/i18n/displayStrings_ro.properties index 6efcf95dfa..d3183e4b3f 100644 --- a/core/src/main/resources/i18n/displayStrings_ro.properties +++ b/core/src/main/resources/i18n/displayStrings_ro.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\nIssuance date: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=Nu ai suficiente fonduri pentru crearea solicitării de despăgubire.\nLipsesc: {0} +dao.proposal.create.missingBsqFunds=Nu ai suficiente fonduri pentru crearea solicitării de despăgubire.\nLipsesc: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 525fe1f430..2335b6b9c4 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Запрос/выдача компенсации dao.tx.issuanceFromCompReq.tooltip=Запрос компенсации, который привел к выпуску новых BSQ.\nДата выпуска: {0} dao.tx.issuanceFromReimbursement=Запрос/выдача возмещения dao.tx.issuanceFromReimbursement.tooltip=Запрос возмещения, который привел к выпуску новых BSQ.\nДата выпуска: {0} -dao.proposal.create.missingFunds=У Вас недостаточно средств для создания предложения.\nНехватает: {0} +dao.proposal.create.missingBsqFunds=У Вас недостаточно средств для создания предложения.\nНехватает: {0} dao.feeTx.confirm=Подтвердить транзакцию {0} dao.feeTx.confirm.details={0} сбор: {1}\nкомиссия майнера: {2} ({3} сатоши/байт)\nРазмер транзакиции: {4} Кб\n\nДействительно хотите опубликовать транзакцию {5}? diff --git a/core/src/main/resources/i18n/displayStrings_sr.properties b/core/src/main/resources/i18n/displayStrings_sr.properties index 6c56e7263b..5fc783659d 100644 --- a/core/src/main/resources/i18n/displayStrings_sr.properties +++ b/core/src/main/resources/i18n/displayStrings_sr.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\nIssuance date: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} +dao.proposal.create.missingBsqFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 209f0b9e46..f9624671b2 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=คำขอหรือการออกค่า dao.tx.issuanceFromCompReq.tooltip=คำขอค่าสินไหมทดแทน ซึ่งนำไปสู่การออก BSQ ใหม่\nวันที่ออก: {0} dao.tx.issuanceFromReimbursement=การออกคำสั่ง/การยื่นคำร้องขอการชำระเงินคืน dao.tx.issuanceFromReimbursement.tooltip=การเรียกร้องขอการชำระเงินคืนซึ่งเป็นคำสั่งภายใต้ BSQ ฉบับใหม่\nวันที่เริ่มทำการ: {0} -dao.proposal.create.missingFunds=คุณไม่มีเงินเพียงพอสำหรับการสร้างข้อเสนอ\nขาดไป: {0} +dao.proposal.create.missingBsqFunds=คุณไม่มีเงินเพียงพอสำหรับการสร้างข้อเสนอ\nขาดไป: {0} dao.feeTx.confirm=ยืนยันการทำรายการ {0} dao.feeTx.confirm.details={0} ค่าธรรมเนียม: {1}\nค่าธรรมเนียมการขุด: {2} ({3} Satoshis / byte)\nขนาดของธุรกรรม: {4} Kb\n\nคุณแน่ใจหรือไม่ว่าต้องการเผยแพร่ {5} ธุรกรรม? diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 8ae18b0b2b..f1d054acef 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=Yêu cầu bồi thường/ban hành dao.tx.issuanceFromCompReq.tooltip=Yêu cầu bồi thường dẫn đến ban hành BSQ mới.\nNgày ban hành: {0} dao.tx.issuanceFromReimbursement=Yêu cầu/ Phát hành bồi hoàn dao.tx.issuanceFromReimbursement.tooltip=Yêu cầu bồi hoàn dẫn đến ban hành BSQ mới.\nNgày ban hành: {0} -dao.proposal.create.missingFunds=Bạn không có đủ tiền để tạo đề xuất.\nThiếu: {0} +dao.proposal.create.missingBsqFunds=Bạn không có đủ tiền để tạo đề xuất.\nThiếu: {0} dao.feeTx.confirm=Xác nhận {0} giao dịch dao.feeTx.confirm.details={0} phí: {1}\nPhí đào: {2} ({3} Satoshis/byte)\nKích thước giao dịch: {4} Kb\n\nBạn có chắc là muốn công bố giao dịch {5}? diff --git a/core/src/main/resources/i18n/displayStrings_zh.properties b/core/src/main/resources/i18n/displayStrings_zh.properties index ccea4cc7c6..865ee58beb 100644 --- a/core/src/main/resources/i18n/displayStrings_zh.properties +++ b/core/src/main/resources/i18n/displayStrings_zh.properties @@ -1551,7 +1551,7 @@ dao.tx.issuanceFromCompReq=补偿请求/发行 dao.tx.issuanceFromCompReq.tooltip=导致新BSQ发行的补偿请求\n发行日期: {0} dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\nIssuance date: {0} -dao.proposal.create.missingFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} +dao.proposal.create.missingBsqFunds=You don''t have sufficient funds for creating the proposal.\nMissing: {0} dao.feeTx.confirm=Confirm {0} transaction dao.feeTx.confirm.details={0} fee: {1}\nMining fee: {2} ({3} Satoshis/byte)\nTransaction size: {4} Kb\n\nAre you sure you want to publish the {5} transaction? diff --git a/desktop/package/linux/package.sh b/desktop/package/linux/package.sh old mode 100644 new mode 100755 diff --git a/desktop/package/linux/release.sh b/desktop/package/linux/release.sh old mode 100644 new mode 100755 diff --git a/desktop/src/main/java/bisq/desktop/app/BisqApp.java b/desktop/src/main/java/bisq/desktop/app/BisqApp.java index 890847d2a5..1920bd5853 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqApp.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqApp.java @@ -37,6 +37,7 @@ import bisq.core.app.AvoidStandbyModeService; import bisq.core.app.BisqEnvironment; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.governance.voteresult.MissingDataRequestService; import bisq.core.filter.FilterManager; import bisq.core.locale.Res; import bisq.core.offer.OpenOfferManager; @@ -281,6 +282,8 @@ public class BisqApp extends Application implements UncaughtExceptionHandler { showSendAlertMessagePopup(injector); } else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) { showFilterPopup(injector); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.UP, keyEvent)) { + injector.getInstance(MissingDataRequestService.class).reRepublishAllGovernanceData(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.T, keyEvent)) { // Toggle between show tor logs and only show warnings. Helpful in case of connection problems String pattern = "org.berndpruenster.netlayer"; diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index a674acd9f8..82f30d010e 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -39,7 +39,6 @@ import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.settings.SettingsView; import bisq.desktop.util.Transitions; -import bisq.core.app.BisqEnvironment; import bisq.core.exceptions.BisqException; import bisq.core.locale.Res; import bisq.core.util.BSFormatter; @@ -184,27 +183,10 @@ public class MainView extends InitializableView { JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge disputesButtonWithBadge = new JFXBadge(disputesButton); - final Region daoButtonSpacer = getNavigationSpacer(); - - if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { - daoButton.setVisible(false); - daoButton.setManaged(false); - daoButtonSpacer.setVisible(false); - daoButtonSpacer.setManaged(false); - } - root.sceneProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { newValue.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { - // TODO can be removed once DAO is released - if (Utilities.isAltOrCtrlPressed(KeyCode.D, keyEvent)) { - if (BisqEnvironment.getBaseCurrencyNetwork().isBitcoin()) { - daoButton.setVisible(true); - daoButton.setManaged(true); - daoButtonSpacer.setVisible(true); - daoButtonSpacer.setManaged(true); - } - } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT1, keyEvent)) { + if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT1, keyEvent)) { marketButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT2, keyEvent)) { buyButton.fire(); @@ -264,7 +246,7 @@ public class MainView extends InitializableView { HBox.setHgrow(primaryNav, Priority.SOMETIMES); HBox secondaryNav = new HBox(disputesButtonWithBadge, getNavigationSpacer(), settingsButton, - getNavigationSpacer(), accountButton, daoButtonSpacer, daoButton); + getNavigationSpacer(), accountButton, getNavigationSpacer(), daoButton); secondaryNav.getStyleClass().add("nav-secondary"); HBox.setHgrow(secondaryNav, Priority.SOMETIMES); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 92a568c2b7..39a143b35c 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -283,7 +283,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList .show(); }); bisqSetup.setVoteResultExceptionHandler(voteResultException -> { - new Popup<>().error(voteResultException.toString()).show(); + log.warn(voteResultException.toString()); }); bisqSetup.setChainFileLockedExceptionHandler(msg -> { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java index 40b44d471a..e0343b3ba0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -30,12 +30,12 @@ import bisq.desktop.main.dao.burnbsq.BurnBsqView; import bisq.desktop.main.dao.governance.GovernanceView; import bisq.desktop.main.dao.wallet.BsqWalletView; import bisq.desktop.main.dao.wallet.dashboard.BsqDashboardView; +import bisq.desktop.main.overlays.popups.Popup; import bisq.core.app.BisqEnvironment; +import bisq.core.dao.governance.votereveal.VoteRevealService; import bisq.core.locale.Res; -import bisq.common.app.DevEnv; - import javax.inject.Inject; import javafx.fxml.FXML; @@ -61,9 +61,15 @@ public class DaoView extends ActivatableViewAndModel { private BsqWalletView bsqWalletView; @Inject - private DaoView(CachingViewLoader viewLoader, Navigation navigation) { + private DaoView(CachingViewLoader viewLoader, VoteRevealService voteRevealService, Navigation navigation) { this.viewLoader = viewLoader; this.navigation = navigation; + + voteRevealService.addVoteRevealTxPublishedListener(txId -> { + new Popup<>().headLine(Res.get("dao.voteReveal.txPublished.headLine")) + .feedback(Res.get("dao.voteReveal.txPublished", txId)) + .show(); + }); } @Override @@ -78,12 +84,13 @@ public class DaoView extends ActivatableViewAndModel { bondingTab.setClosable(false); burnBsqTab.setClosable(false); - root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab); - - if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq() || !DevEnv.isDaoPhase2Activated()) { - bondingTab.setDisable(true); + if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { proposalsTab.setDisable(true); + bondingTab.setDisable(true); burnBsqTab.setDisable(true); + root.getTabs().addAll(bsqWalletTab); + } else { + root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab); } navigationListener = viewPath -> { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java index 235f19322a..7284439937 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondListItem.java @@ -44,7 +44,6 @@ class BondListItem { private final String bondDetails; private final BondState bondState; private final String bondStateString; - private final Date lockupDate; BondListItem(Bond bond, BsqFormatter bsqFormatter) { this.bond = bond; @@ -59,8 +58,7 @@ class BondListItem { bondDetails = Utilities.bytesAsHexString(bond.getBondedAsset().getHash()); } lockupTxId = bond.getLockupTxId(); - lockupDate = new Date(bond.getLockupDate()); - lockupDateString = bsqFormatter.formatDateTime(lockupDate); + lockupDateString = bond.getLockupDate() > 0 ? bsqFormatter.formatDateTime(new Date(bond.getLockupDate())) : "-"; bondState = bond.getBondState(); bondStateString = Res.get("dao.bond.bondState." + bond.getBondState().name()); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java index 4f140387d2..11331355d6 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java @@ -173,7 +173,7 @@ public class ProofOfBurnView extends ActivatableView implements if (!DevEnv.isDevMode()) { GUIUtil.showBsqFeeInfoPopup(amount, miningFee, txSize, bsqFormatter, btcFormatter, - Res.get("dao.proofOfBurn.amount"), () -> doPublishFeeTx(transaction, preImageAsString)); + Res.get("dao.proofOfBurn.header"), () -> doPublishFeeTx(transaction, preImageAsString)); } else { doPublishFeeTx(transaction, preImageAsString); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java index 14d9dbbb82..8b044b71d7 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java @@ -21,6 +21,7 @@ import bisq.desktop.components.SeparatedPhaseBars; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.locale.Res; @@ -31,9 +32,6 @@ import javafx.scene.layout.GridPane; import javafx.geometry.Insets; -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; - import java.util.Arrays; import java.util.List; @@ -44,13 +42,14 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg; @Slf4j public class PhasesView implements DaoStateListener { private final DaoFacade daoFacade; + private final PeriodService periodService; private SeparatedPhaseBars separatedPhaseBars; private List phaseBarsItems; - private Subscription phaseSubscription; @Inject - private PhasesView(DaoFacade daoFacade) { + private PhasesView(DaoFacade daoFacade, PeriodService periodService) { this.daoFacade = daoFacade; + this.periodService = periodService; } public int addGroup(GridPane gridPane, int gridRow) { @@ -65,24 +64,11 @@ public class PhasesView implements DaoStateListener { public void activate() { daoFacade.addBsqStateListener(this); - phaseSubscription = EasyBind.subscribe(daoFacade.phaseProperty(), phase -> { - phaseBarsItems.forEach(item -> { - if (item.getPhase() == phase) { - item.setActive(); - } else { - item.setInActive(); - } - }); - - }); - applyData(daoFacade.getChainHeight()); } public void deactivate() { daoFacade.removeBsqStateListener(this); - - phaseSubscription.unsubscribe(); } @@ -93,6 +79,19 @@ public class PhasesView implements DaoStateListener { @Override public void onNewBlockHeight(int height) { applyData(height); + + phaseBarsItems.forEach(item -> { + DaoPhase.Phase phase = item.getPhase(); + // Last block is considered for the break as we must not publish a tx there (would get confirmed in next + // block which would be a break). Only at result phase we don't have that situation ans show the last block + // as valid block in the phase. + if (periodService.isInPhaseButNotLastBlock(phase) || + (phase == DaoPhase.Phase.RESULT && periodService.isInPhase(height, phase))) { + item.setActive(); + } else { + item.setInActive(); + } + }); } @Override @@ -131,6 +130,7 @@ public class PhasesView implements DaoStateListener { } else if (height > lastBlock) { progress = 1; } + item.getProgressProperty().set(progress); }); separatedPhaseBars.updateWidth(); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java index b0a2cbc268..40221e4163 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -52,6 +52,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.util.BsqFormatter; import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.UrlInputValidator; import bisq.asset.Asset; @@ -202,7 +203,7 @@ public class ProposalDisplay { Res.get("dao.proposal.display.link")); linkInputTextField.setPromptText(Res.get("dao.proposal.display.link.prompt")); if (isMakeProposalScreen) - linkInputTextField.setValidator(new InputValidator()); + linkInputTextField.setValidator(new UrlInputValidator()); inputControls.add(linkInputTextField); Tuple3 tuple = FormBuilder.addTopLabelHyperlinkWithIcon(gridPane, gridRow, @@ -459,6 +460,14 @@ public class ProposalDisplay { myVoteTextField.setManaged(show); } + public void setIsVoteIncludedInResult(boolean isVoteIncludedInResult) { + if (!isVoteIncludedInResult && myVoteTextField != null && !myVoteTextField.getText().isEmpty()) { + String text = myVoteTextField.getText(); + myVoteTextField.setText(Res.get("dao.proposal.myVote.invalid") + " - " + text); + myVoteTextField.getStyleClass().add("error-text"); + } + } + public void applyProposalPayload(Proposal proposal) { proposalTypeTextField.setText(proposal.getType().getDisplayName()); nameTextField.setText(proposal.getName()); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java index 5c68738af4..af1fd41ce1 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/dashboard/GovernanceDashboardView.java @@ -24,6 +24,7 @@ import bisq.desktop.util.FormBuilder; import bisq.desktop.util.Layout; import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.model.governance.DaoPhase; import bisq.core.locale.Res; @@ -48,6 +49,7 @@ import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField; @FxmlView public class GovernanceDashboardView extends ActivatableView implements DaoStateListener { private final DaoFacade daoFacade; + private final PeriodService periodService; private final PhasesView phasesView; private final BSFormatter formatter; @@ -60,8 +62,9 @@ public class GovernanceDashboardView extends ActivatableView imp /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public GovernanceDashboardView(DaoFacade daoFacade, PhasesView phasesView, BSFormatter formatter) { + public GovernanceDashboardView(DaoFacade daoFacade, PeriodService periodService, PhasesView phasesView, BSFormatter formatter) { this.daoFacade = daoFacade; + this.periodService = periodService; this.phasesView = phasesView; this.formatter = formatter; } @@ -121,7 +124,13 @@ public class GovernanceDashboardView extends ActivatableView imp private void applyData(int height) { currentBlockHeightTextField.setText(String.valueOf(daoFacade.getChainHeight())); - currentPhaseTextField.setText(Res.get("dao.phase." + daoFacade.phaseProperty().get().name())); + DaoPhase.Phase phase = daoFacade.phaseProperty().get(); + // If we are in last block of proposal, blindVote or voteReveal phase we show following break. + if (!periodService.isInPhaseButNotLastBlock(phase) && + (phase == DaoPhase.Phase.PROPOSAL || phase == DaoPhase.Phase.BLIND_VOTE || phase == DaoPhase.Phase.VOTE_REVEAL)) { + phase = periodService.getPhaseForHeight(height + 1); + } + currentPhaseTextField.setText(Res.get("dao.phase." + phase.name())); proposalTextField.setText(getPhaseDuration(height, DaoPhase.Phase.PROPOSAL)); blindVoteTextField.setText(getPhaseDuration(height, DaoPhase.Phase.BLIND_VOTE)); voteRevealTextField.setText(getPhaseDuration(height, DaoPhase.Phase.VOTE_REVEAL)); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java index 9b946631fd..7324c4cae4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -215,9 +215,14 @@ public class MakeProposalView extends ActivatableView implements doPublishMyProposal(proposal, transaction); } } catch (InsufficientMoneyException e) { - BSFormatter formatter = e instanceof InsufficientBsqException ? bsqFormatter : btcFormatter; - new Popup<>().warning(Res.get("dao.proposal.create.missingFunds", - formatter.formatCoinWithCode(e.missing))).show(); + if (e instanceof InsufficientBsqException) { + new Popup<>().warning(Res.get("dao.proposal.create.missingBsqFunds", + bsqFormatter.formatCoinWithCode(e.missing))).show(); + } else { + new Popup<>().warning(Res.get("dao.proposal.create.missingMinerFeeFunds", + btcFormatter.formatCoinWithCode(e.missing))).show(); + } + } catch (ValidationException e) { String message; if (e.getMinRequestAmount() != null) { @@ -238,13 +243,16 @@ public class MakeProposalView extends ActivatableView implements daoFacade.publishMyProposal(proposal, transaction, () -> { - if (proposalDisplay != null) - proposalDisplay.clearForm(); - proposalTypeComboBox.getSelectionModel().clearSelection(); if (!DevEnv.isDevMode()) - new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + new Popup<>().feedback(Res.get("dao.tx.published.success")).show(); }, errorMessage -> new Popup<>().warning(errorMessage).show()); + + // We reset UI without waiting for callback as callback might be slow and then the user could create multiple + // proposals. + if (proposalDisplay != null) + proposalDisplay.clearForm(); + proposalTypeComboBox.getSelectionModel().clearSelection(); } @Nullable @@ -253,18 +261,20 @@ public class MakeProposalView extends ActivatableView implements checkNotNull(proposalDisplay, "proposalDisplay must not be null"); + String link = proposalDisplay.linkInputTextField.getText(); + String name = proposalDisplay.nameTextField.getText(); switch (type) { case COMPENSATION_REQUEST: checkNotNull(proposalDisplay.requestedBsqTextField, "proposalDisplay.requestedBsqTextField must not be null"); - return daoFacade.getCompensationProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), + return daoFacade.getCompensationProposalWithTransaction(name, + link, bsqFormatter.parseToCoin(proposalDisplay.requestedBsqTextField.getText())); case REIMBURSEMENT_REQUEST: checkNotNull(proposalDisplay.requestedBsqTextField, "proposalDisplay.requestedBsqTextField must not be null"); - return daoFacade.getReimbursementProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), + return daoFacade.getReimbursementProposalWithTransaction(name, + link, bsqFormatter.parseToCoin(proposalDisplay.requestedBsqTextField.getText())); case CHANGE_PARAM: checkNotNull(proposalDisplay.paramComboBox, @@ -284,8 +294,8 @@ public class MakeProposalView extends ActivatableView implements log.info("Change param: paramValue={}, paramValueAsString={}", paramValue, paramValueAsString); changeParamValidator.validateParamValue(selectedParam, paramValue); - return daoFacade.getParamProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), + return daoFacade.getParamProposalWithTransaction(name, + link, selectedParam, paramValue); } catch (Throwable e) { @@ -295,27 +305,22 @@ public class MakeProposalView extends ActivatableView implements case BONDED_ROLE: checkNotNull(proposalDisplay.bondedRoleTypeComboBox, "proposalDisplay.bondedRoleTypeComboBox must not be null"); - Role role = new Role(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), + Role role = new Role(name, + link, proposalDisplay.bondedRoleTypeComboBox.getSelectionModel().getSelectedItem()); return daoFacade.getBondedRoleProposalWithTransaction(role); case CONFISCATE_BOND: checkNotNull(proposalDisplay.confiscateBondComboBox, "proposalDisplay.confiscateBondComboBox must not be null"); Bond bond = proposalDisplay.confiscateBondComboBox.getSelectionModel().getSelectedItem(); - return daoFacade.getConfiscateBondProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), - bond.getLockupTxId()); + return daoFacade.getConfiscateBondProposalWithTransaction(name, link, bond.getLockupTxId()); case GENERIC: - return daoFacade.getGenericProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText()); + return daoFacade.getGenericProposalWithTransaction(name, link); case REMOVE_ASSET: checkNotNull(proposalDisplay.assetComboBox, "proposalDisplay.assetComboBox must not be null"); Asset asset = proposalDisplay.assetComboBox.getSelectionModel().getSelectedItem(); - return daoFacade.getRemoveAssetProposalWithTransaction(proposalDisplay.nameTextField.getText(), - proposalDisplay.linkInputTextField.getText(), - asset); + return daoFacade.getRemoveAssetProposalWithTransaction(name, link, asset); default: final String msg = "Undefined ProposalType " + selectedProposalType; log.error(msg); @@ -372,9 +377,12 @@ public class MakeProposalView extends ActivatableView implements .filter(Objects::nonNull).forEach(comboBox -> { inputsValid.set(inputsValid.get() && comboBox.getSelectionModel().getSelectedItem() != null); }); + + InputTextField linkInputTextField = proposalDisplay.linkInputTextField; + inputsValid.set(inputsValid.get() && + linkInputTextField.getValidator().validate(linkInputTextField.getText()).isValid); } makeProposalButton.setDisable(!inputsValid.get()); } } - diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java index 76ce50cbbc..0fd5b0675c 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -50,6 +50,7 @@ import bisq.core.dao.state.model.governance.EvaluatedProposal; import bisq.core.dao.state.model.governance.Proposal; import bisq.core.dao.state.model.governance.Vote; import bisq.core.locale.Res; +import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; @@ -111,6 +112,7 @@ public class ProposalsView extends ActivatableView implements Bs private final PhasesView phasesView; private final DaoStateService daoStateService; private final ChangeParamValidator changeParamValidator; + private final Preferences preferences; private final BsqFormatter bsqFormatter; private final BSFormatter btcFormatter; @@ -154,6 +156,7 @@ public class ProposalsView extends ActivatableView implements Bs PhasesView phasesView, DaoStateService daoStateService, ChangeParamValidator changeParamValidator, + Preferences preferences, BsqFormatter bsqFormatter, BSFormatter btcFormatter) { this.daoFacade = daoFacade; @@ -161,6 +164,7 @@ public class ProposalsView extends ActivatableView implements Bs this.phasesView = phasesView; this.daoStateService = daoStateService; this.changeParamValidator = changeParamValidator; + this.preferences = preferences; this.bsqFormatter = bsqFormatter; this.btcFormatter = btcFormatter; } @@ -514,17 +518,19 @@ public class ProposalsView extends ActivatableView implements Bs voteButtonInfoLabel.setText(Res.get("dao.blindVote.startPublishing")); daoFacade.publishBlindVote(stake, () -> { - voteButtonBusyAnimation.stop(); - voteButtonInfoLabel.setText(""); if (!DevEnv.isDevMode()) new Popup<>().feedback(Res.get("dao.blindVote.success")).show(); - - updateViews(); }, exception -> { voteButtonBusyAnimation.stop(); voteButtonInfoLabel.setText(""); new Popup<>().warning(exception.toString()).show(); }); + + // We reset UI without waiting for callback as callback might be slow and then the user could click + // multiple times. + voteButtonBusyAnimation.stop(); + voteButtonInfoLabel.setText(""); + updateViews(); } private void updateStateAfterVote() { @@ -613,7 +619,9 @@ public class ProposalsView extends ActivatableView implements Bs } else { String msg = "We found multiple MyVote entries in that cycle. That is not supported by the UI."; log.warn(msg); - new Popup<>().error(msg).show(); + String id = "multipleVotes"; + if (preferences.showAgain(id)) + new Popup<>().warning(msg).dontShowAgainId(id).show(); } voteButton.setVisible(false); voteButton.setManaged(false); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java index 81e8a08c6b..b4ad2f3051 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/result/VoteResultView.java @@ -25,6 +25,7 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.TableGroupHeadline; import bisq.desktop.main.dao.governance.PhasesView; import bisq.desktop.main.dao.governance.ProposalDisplay; +import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; @@ -32,11 +33,13 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.period.CycleService; import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.governance.voteresult.VoteResultException; import bisq.core.dao.governance.voteresult.VoteResultService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.governance.Ballot; +import bisq.core.dao.state.model.governance.Cycle; import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; import bisq.core.dao.state.model.governance.EvaluatedProposal; import bisq.core.dao.state.model.governance.Proposal; @@ -56,6 +59,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; +import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; @@ -146,7 +150,6 @@ public class VoteResultView extends ActivatableView implements D createCyclesTable(); } - @Override protected void activate() { super.activate(); @@ -203,6 +206,7 @@ public class VoteResultView extends ActivatableView implements D if (item != null) { resultsOfCycle = item.getResultsOfCycle(); + maybeShowVoteResultErrors(item.getResultsOfCycle().getCycle()); createProposalsTable(); selectedProposalSubscription = EasyBind.subscribe(proposalsTableView.getSelectionModel().selectedItemProperty(), @@ -210,22 +214,48 @@ public class VoteResultView extends ActivatableView implements D } } + private void maybeShowVoteResultErrors(Cycle cycle) { + List exceptions = voteResultService.getVoteResultExceptions().stream() + .filter(voteResultException -> cycle.getHeightOfFirstBlock() == voteResultException.getHeightOfFirstBlockInCycle()) + .collect(Collectors.toList()); + if (!exceptions.isEmpty()) { + TextArea textArea = FormBuilder.addTextArea(root, ++gridRow, ""); + GridPane.setMargin(textArea, new Insets(Layout.GROUP_DISTANCE, -15, 0, -10)); + textArea.setPrefHeight(100); + + StringBuilder sb = new StringBuilder(Res.getWithCol("dao.results.exceptions") + "\n"); + exceptions.forEach(exception -> { + if (exception.getCause() != null) + sb.append(exception.getCause().getMessage()); + else + sb.append(exception.getMessage()); + sb.append("\n"); + }); + + textArea.setText(sb.toString()); + } + } + private void onSelectProposalResultListItem(ProposalListItem item) { selectedProposalListItem = item; - GUIUtil.removeChildrenFromGridPaneRows(root, 3, gridRow); + GUIUtil.removeChildrenFromGridPaneRows(root, 4, gridRow); gridRow = 2; if (selectedProposalListItem != null) { - EvaluatedProposal evaluatedProposal = selectedProposalListItem.getEvaluatedProposal(); Optional optionalBallot = daoFacade.getAllValidBallots().stream() .filter(ballot -> ballot.getTxId().equals(evaluatedProposal.getProposalTxId())) .findAny(); Ballot ballot = optionalBallot.orElse(null); - createProposalDisplay(evaluatedProposal, ballot); + ProposalDisplay proposalDisplay = createProposalDisplay(evaluatedProposal, ballot); createVotesTable(); + + // Check if my vote is included in result + boolean isVoteIncludedInResult = voteListItemList.stream() + .anyMatch(voteListItem -> bsqWalletService.getTransaction(voteListItem.getBlindVoteTxId()) != null); + proposalDisplay.setIsVoteIncludedInResult(isVoteIncludedInResult); } } @@ -350,7 +380,7 @@ public class VoteResultView extends ActivatableView implements D // Create views: proposalDisplay /////////////////////////////////////////////////////////////////////////////////////////// - private void createProposalDisplay(EvaluatedProposal evaluatedProposal, Ballot ballot) { + private ProposalDisplay createProposalDisplay(EvaluatedProposal evaluatedProposal, Ballot ballot) { Proposal proposal = evaluatedProposal.getProposal(); ProposalDisplay proposalDisplay = new ProposalDisplay(new GridPane(), bsqFormatter, daoFacade, null); @@ -373,6 +403,7 @@ public class VoteResultView extends ActivatableView implements D long merit = meritAndStakeTuple.first; long stake = meritAndStakeTuple.second; proposalDisplay.applyBallotAndVoteWeight(ballot, merit, stake); + return proposalDisplay; } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/receive/BsqReceiveView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/receive/BsqReceiveView.java index 148975bddd..72f8f138fc 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/receive/BsqReceiveView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/receive/BsqReceiveView.java @@ -22,8 +22,11 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.BsqAddressTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; +import bisq.core.app.BisqEnvironment; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.locale.Res; import bisq.core.util.BsqFormatter; @@ -32,6 +35,7 @@ import bisq.common.util.Tuple3; import javax.inject.Inject; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; @@ -61,28 +65,62 @@ public class BsqReceiveView extends ActivatableView { @Override public void initialize() { - gridRow = bsqBalanceUtil.addGroup(root, gridRow); + if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { + gridRow = bsqBalanceUtil.addGroup(root, gridRow); - TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 1, - Res.get("dao.wallet.receive.fundYourWallet"), Layout.GROUP_DISTANCE); - GridPane.setColumnSpan(titledGroupBg, 3); - Tuple3 tuple = addLabelBsqAddressTextField(root, gridRow, - Res.get("dao.wallet.receive.bsqAddress"), - Layout.FIRST_ROW_AND_GROUP_DISTANCE); - addressTextField = tuple.second; - GridPane.setColumnSpan(tuple.third, 3); + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 1, + Res.get("dao.wallet.receive.fundYourWallet"), Layout.GROUP_DISTANCE); + GridPane.setColumnSpan(titledGroupBg, 3); + Tuple3 tuple = addLabelBsqAddressTextField(root, gridRow, + Res.get("dao.wallet.receive.bsqAddress"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addressTextField = tuple.second; + GridPane.setColumnSpan(tuple.third, 3); + } else { + addTitledGroupBg(root, gridRow, 6, + Res.get("dao.wallet.receive.dao.headline"), 0); + FormBuilder.addMultilineLabel(root, gridRow, Res.get("dao.wallet.receive.daoInfo"), 10); + + Button daoInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoInfo.button")); + daoInfoButton.setOnAction(e -> { + GUIUtil.openWebPage("https://bisq.network/dao"); + }); + + FormBuilder.addMultilineLabel(root, ++gridRow, Res.get("dao.wallet.receive.daoTestnetInfo")); + Button daoContributorInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoTestnetInfo.button")); + daoContributorInfoButton.setOnAction(e -> { + GUIUtil.openWebPage("https://bisq.network/dao-testnet"); + }); + + FormBuilder.addMultilineLabel(root, ++gridRow, Res.get("dao.wallet.receive.daoContributorInfo")); + + Button daoTestnetInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoContributorInfo.button")); + daoTestnetInfoButton.setOnAction(e -> { + GUIUtil.openWebPage("https://bisq.network/dao-genesis"); + }); + + addTitledGroupBg(root, ++gridRow, 1, + Res.get("dao.wallet.receive.fundYourWallet"), 20); + Tuple3 tuple = addLabelBsqAddressTextField(root, gridRow, + Res.get("dao.wallet.receive.bsqAddress"), + 40); + addressTextField = tuple.second; + GridPane.setColumnSpan(tuple.third, 3); + } } @Override protected void activate() { - bsqBalanceUtil.activate(); + if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) + bsqBalanceUtil.activate(); addressTextField.setAddress(bsqFormatter.getBsqAddressStringFromAddress(bsqWalletService.getUnusedAddress())); } @Override protected void deactivate() { - bsqBalanceUtil.deactivate(); + if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) + bsqBalanceUtil.deactivate(); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index a10ec5fc6c..ac137b1742 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -244,6 +244,7 @@ public class BsqSendView extends ActivatableView implements BsqB txSize, receiversAddressInputTextField.getText(), bsqFormatter, + btcFormatter, () -> { receiversAddressInputTextField.setText(""); amountInputTextField.setText(""); @@ -298,6 +299,7 @@ public class BsqSendView extends ActivatableView implements BsqB miningFee, txSize, receiversBtcAddressInputTextField.getText(), btcFormatter, + btcFormatter, () -> { receiversBtcAddressInputTextField.setText(""); btcAmountInputTextField.setText(""); @@ -330,16 +332,17 @@ public class BsqSendView extends ActivatableView implements BsqB Transaction txWithBtcFee, Coin miningFee, int txSize, String address, - BSFormatter formatter, + BSFormatter amountFormatter, // can be BSQ or BTC formatter + BSFormatter feeFormatter, ResultHandler resultHandler) { new Popup<>().headLine(Res.get("dao.wallet.send.sendFunds.headline")) .confirmation(Res.get("dao.wallet.send.sendFunds.details", - formatter.formatCoinWithCode(receiverAmount), + amountFormatter.formatCoinWithCode(receiverAmount), address, - formatter.formatCoinWithCode(miningFee), + feeFormatter.formatCoinWithCode(miningFee), CoinUtil.getFeePerByte(miningFee, txSize), txSize / 1000d, - formatter.formatCoinWithCode(receiverAmount))) + amountFormatter.formatCoinWithCode(receiverAmount))) .actionButtonText(Res.get("shared.yes")) .onAction(() -> { walletsManager.publishAndCommitBsqTx(txWithBtcFee, new TxBroadcaster.Callback() { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java index 68e1464786..e8b175c9bf 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java @@ -343,6 +343,7 @@ public abstract class Overlay { public T error(String message) { type = Type.Error; showReportErrorButtons(); + width = 1100; if (headLine == null) this.headLine = Res.get("popup.headline.error"); this.message = message; diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java index 6fb693d22c..5fd771c924 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java @@ -25,21 +25,18 @@ import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.Layout; +import bisq.core.app.TorSetup; import bisq.core.locale.Res; import bisq.core.user.Preferences; -import bisq.network.NetworkOptionKeys; import bisq.network.p2p.network.DefaultPluggableTransports; import bisq.network.p2p.network.NetworkNode; import bisq.common.UserThread; -import bisq.common.storage.FileUtil; import bisq.common.util.Tuple2; import bisq.common.util.Tuple4; import bisq.common.util.Utilities; -import com.google.inject.name.Named; - import javax.inject.Inject; import javafx.scene.Scene; @@ -65,9 +62,6 @@ import javafx.util.StringConverter; import java.net.URI; -import java.nio.file.Paths; - -import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -94,8 +88,8 @@ public class TorNetworkSettingsWindow extends Overlay } private final Preferences preferences; - private NetworkNode networkNode; - private final File torDir; + private final NetworkNode networkNode; + private final TorSetup torSetup; private Label enterBridgeLabel; private ComboBox transportTypeComboBox; private TextArea bridgeEntriesTextArea; @@ -106,10 +100,10 @@ public class TorNetworkSettingsWindow extends Overlay @Inject public TorNetworkSettingsWindow(Preferences preferences, NetworkNode networkNode, - @Named(NetworkOptionKeys.TOR_DIR) File torDir) { + TorSetup torSetup) { this.preferences = preferences; this.networkNode = networkNode; - this.torDir = torDir; + this.torSetup = torSetup; type = Type.Attention; @@ -342,15 +336,7 @@ public class TorNetworkSettingsWindow extends Overlay networkNode.shutDown(() -> { // We give it a bit extra time to be sure that OS locks are removed UserThread.runAfter(() -> { - final File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()); - try { - FileUtil.deleteDirectory(torDir, hiddenservice, true); - resultHandler.run(); - } catch (IOException e) { - e.printStackTrace(); - log.error(e.toString()); - new Popup<>().error(e.toString()).show(); - } + torSetup.cleanupTorFiles(resultHandler, errorMessage -> new Popup<>().error(errorMessage).show()); }, 3); }); } diff --git a/monitor/src/main/java/bisq/monitor/AvailableTor.java b/monitor/src/main/java/bisq/monitor/AvailableTor.java new file mode 100644 index 0000000000..bd76620c40 --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/AvailableTor.java @@ -0,0 +1,50 @@ +/* + * 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.monitor; + +import java.io.File; +import org.berndpruenster.netlayer.tor.Tor; +import bisq.network.p2p.network.TorMode; + +/** + * This class uses an already defined Tor via Tor.getDefault() + * + * @author Florian Reimair + * + */ +public class AvailableTor extends TorMode { + + private String hiddenServiceDirectory; + + public AvailableTor(File torWorkingDirectory, String hiddenServiceDirectory) { + super(torWorkingDirectory); + + this.hiddenServiceDirectory = hiddenServiceDirectory; + } + + @Override + public Tor getTor() { + return Tor.getDefault(); + } + + @Override + public String getHiddenServiceDirectory() { + return hiddenServiceDirectory; + } + +} diff --git a/monitor/src/main/java/bisq/monitor/Metric.java b/monitor/src/main/java/bisq/monitor/Metric.java index ed537e7784..1417cb29db 100644 --- a/monitor/src/main/java/bisq/monitor/Metric.java +++ b/monitor/src/main/java/bisq/monitor/Metric.java @@ -78,7 +78,7 @@ public abstract class Metric extends Configurable implements Runnable { disable(); } - protected boolean enabled() { + boolean enabled() { return !shutdown; } @@ -117,9 +117,14 @@ public abstract class Metric extends Configurable implements Runnable { while (!shutdown) { // if not, execute all the things synchronized (this) { + log.info("{} started", getName()); execute(); + log.info("{} done", getName()); } + if (shutdown) + continue; + // make sure our configuration is not changed in the moment we want to query it String interval; synchronized (this) { @@ -148,7 +153,7 @@ public abstract class Metric extends Configurable implements Runnable { shutdown = true; } - protected void join() throws InterruptedException { + void join() throws InterruptedException { thread.join(); } diff --git a/monitor/src/main/java/bisq/monitor/Monitor.java b/monitor/src/main/java/bisq/monitor/Monitor.java index f69bfd9739..33fb9d06e3 100644 --- a/monitor/src/main/java/bisq/monitor/Monitor.java +++ b/monitor/src/main/java/bisq/monitor/Monitor.java @@ -17,6 +17,9 @@ package bisq.monitor; +import bisq.monitor.metric.P2PNetworkLoad; +import bisq.monitor.metric.P2PNetworkMessageSnapshot; +import bisq.monitor.metric.P2PRoundTripTime; import bisq.monitor.metric.TorHiddenServiceStartupTime; import bisq.monitor.metric.TorRoundTripTime; import bisq.monitor.metric.TorStartupTime; @@ -48,6 +51,7 @@ import sun.misc.Signal; @Slf4j public class Monitor { + public static final File TOR_WORKING_DIR = new File("monitor/monitor-tor"); private static String[] args = {}; public static void main(String[] args) throws Throwable { @@ -63,11 +67,12 @@ public class Monitor { /** * Starts up all configured Metrics. * - * @throws Exception + * @throws Throwable in case something goes wrong */ private void start() throws Throwable { + // start Tor - Tor.setDefault(new NativeTor(new File("monitor/monitor-tor"), null, null, false)); + Tor.setDefault(new NativeTor(TOR_WORKING_DIR, null, null, false)); // assemble Metrics // - create reporters @@ -78,12 +83,13 @@ public class Monitor { metrics.add(new TorStartupTime(graphiteReporter)); metrics.add(new TorRoundTripTime(graphiteReporter)); metrics.add(new TorHiddenServiceStartupTime(graphiteReporter)); + metrics.add(new P2PRoundTripTime(graphiteReporter)); + metrics.add(new P2PNetworkLoad(graphiteReporter)); + metrics.add(new P2PNetworkMessageSnapshot(graphiteReporter)); // prepare configuration reload // Note that this is most likely only work on Linux - Signal.handle(new Signal("USR1"), signal -> { - reload(); - }); + Signal.handle(new Signal("USR1"), signal -> reload()); // configure Metrics // - which also starts the metrics if appropriate @@ -133,7 +139,6 @@ public class Monitor { for (Metric current : metrics) current.configure(properties); } catch (Exception e) { - // TODO Auto-generated catch block e.printStackTrace(); } } @@ -142,7 +147,7 @@ public class Monitor { * Overloads a default set of properties with a file if given * * @return a set of properties - * @throws Exception + * @throws Exception in case something goes wrong */ private Properties getProperties() throws Exception { Properties defaults = new Properties(); diff --git a/monitor/src/main/java/bisq/monitor/OnionParser.java b/monitor/src/main/java/bisq/monitor/OnionParser.java new file mode 100644 index 0000000000..53e1e5790d --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/OnionParser.java @@ -0,0 +1,47 @@ +/* + * 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.monitor; + +import java.net.MalformedURLException; +import java.net.URL; + +import bisq.network.p2p.NodeAddress; + +/** + * Helper for parsing and pretty printing onion addresses. + * + * @author Florian Reimair + */ +public class OnionParser { + + public static NodeAddress getNodeAddress(final String current) throws MalformedURLException { + String nodeAddress = current.trim(); + if (!nodeAddress.startsWith("http://")) + nodeAddress = "http://" + nodeAddress; + URL tmp = new URL(nodeAddress); + return new NodeAddress(tmp.getHost(), tmp.getPort()); + } + + public static String prettyPrint(final NodeAddress host) { + return host.getHostNameWithoutPostFix(); + } + + public static String prettyPrint(String host) throws MalformedURLException { + return prettyPrint(getNodeAddress(host)); + } +} diff --git a/monitor/src/main/java/bisq/monitor/StatisticsHelper.java b/monitor/src/main/java/bisq/monitor/StatisticsHelper.java new file mode 100644 index 0000000000..e16c0c84d6 --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/StatisticsHelper.java @@ -0,0 +1,66 @@ +/* + * 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.monitor; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.LongSummaryStatistics; +import java.util.Map; + +/** + * Calculates average, max, min, p25, p50, p75 off of a list of samples and + * throws in the sample size for good measure. + * + * @author Florian Reimair + */ +public class StatisticsHelper { + + public static Map process(List samples) { + + // aftermath + Collections.sort(samples); + + // - average, max, min , sample size + LongSummaryStatistics statistics = samples.stream().mapToLong(val -> val).summaryStatistics(); + + Map results = new HashMap<>(); + results.put("average", String.valueOf(Math.round(statistics.getAverage()))); + results.put("max", String.valueOf(statistics.getMax())); + results.put("min", String.valueOf(statistics.getMin())); + results.put("sampleSize", String.valueOf(statistics.getCount())); + + // - p25, median, p75 + Integer[] percentiles = new Integer[] { 25, 50, 75 }; + for (Integer percentile : percentiles) { + double rank = statistics.getCount() * percentile / 100; + Long percentileValue; + if (samples.size() <= rank + 1) + percentileValue = samples.get(samples.size() - 1); + else if (Math.floor(rank) == rank) + percentileValue = samples.get((int) rank); + else + percentileValue = Math.round(samples.get((int) Math.floor(rank)) + + (samples.get((int) (Math.floor(rank) + 1)) - samples.get((int) Math.floor(rank))) + / (rank - Math.floor(rank))); + results.put("p" + percentile, String.valueOf(percentileValue)); + } + + return results; + } +} diff --git a/monitor/src/main/java/bisq/monitor/ThreadGate.java b/monitor/src/main/java/bisq/monitor/ThreadGate.java new file mode 100644 index 0000000000..8c9fa27c61 --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/ThreadGate.java @@ -0,0 +1,81 @@ +/* + * 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.monitor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +/** + * Gate pattern to help with thread synchronization + * + * @author Florian Reimair + */ +@Slf4j +public class ThreadGate { + + private CountDownLatch lock = new CountDownLatch(0); + + /** + * Make everyone wait until the gate is open again. + */ + public void engage() { + lock = new CountDownLatch(1); + } + + /** + * Make everyone wait until the gate is open again. + * + * @param numberOfLocks how often the gate has to be unlocked until the gate + * opens. + */ + public void engage(int numberOfLocks) { + lock = new CountDownLatch(numberOfLocks); + } + + /** + * Wait for the gate to be opened. Blocks until the gate is open again. Returns + * immediately if the gate is already open. + */ + public synchronized void await() { + while (lock.getCount() > 0) + try { + if (!lock.await(90, TimeUnit.SECONDS)) { + log.warn("timeout occured!"); + break; // break the loop + } + } catch (InterruptedException ignore) { + } + } + + /** + * Open the gate and let everyone proceed with their execution. + */ + public void proceed() { + lock.countDown(); + } + + /** + * Open the gate with no regards on how many locks are still in place. + */ + public void unlock() { + while (lock.getCount() > 0) + lock.countDown(); + } +} diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java new file mode 100644 index 0000000000..4c6a171515 --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java @@ -0,0 +1,279 @@ +/* + * 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.monitor.metric; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.jetbrains.annotations.NotNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.monitor.AvailableTor; +import bisq.monitor.Metric; +import bisq.monitor.Monitor; +import bisq.monitor.OnionParser; +import bisq.monitor.Reporter; +import bisq.monitor.ThreadGate; +import bisq.network.p2p.CloseConnectionMessage; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.SetupListener; +import bisq.network.p2p.network.TorNetworkNode; +import bisq.network.p2p.peers.getdata.messages.GetDataResponse; +import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; +import lombok.extern.slf4j.Slf4j; + +/** + * Contacts a list of hosts and asks them for all the data we do not have. The + * answers are then compiled into buckets of message types. Based on these + * buckets, the Metric reports (for each host) the message types observed and + * their number along with a relative comparison between all hosts. + * + * @author Florian Reimair + * + */ +@Slf4j +public class P2PNetworkLoad extends Metric implements MessageListener, SetupListener { + + private static final String HOSTS = "run.hosts"; + private static final String TOR_PROXY_PORT = "run.torProxyPort"; + private NetworkNode networkNode; + private final File torHiddenServiceDir = new File("metric_p2pNetworkLoad"); + private int nonce; + private Map> bucketsPerHost = new ConcurrentHashMap<>(); + private Set hashes = new HashSet<>(); + private final ThreadGate hsReady = new ThreadGate(); + private final ThreadGate gate = new ThreadGate(); + + /** + * Efficient way to count message occurrences. + */ + private class Counter { + private int value = 0; + + int value() { + return value; + } + + void increment() { + value++; + } + } + + public P2PNetworkLoad(Reporter reporter) { + super(reporter); + + Version.setBaseCryptoNetworkId(0); // set to BTC_MAINNET + } + + @Override + protected void execute() { + // in case we do not have a NetworkNode up and running, we create one + if (null == networkNode) { + // prepare the gate + hsReady.engage(); + + // start the network node + networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9053")), + new CoreNetworkProtoResolver(), false, + new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName())); + networkNode.start(this); + + // wait for the HS to be published + hsReady.await(); + } + + // clear our buckets + bucketsPerHost.clear(); + ArrayList threadList = new ArrayList<>(); + + // for each configured host + for (String current : configuration.getProperty(HOSTS, "").split(",")) { + threadList.add(new Thread(() -> { + try { + // parse Url + NodeAddress target = OnionParser.getNodeAddress(current); + + // do the data request + nonce = new Random().nextInt(); + SettableFuture future = networkNode.sendMessage(target, + new PreliminaryGetDataRequest(nonce, hashes)); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + connection.addMessageListener(P2PNetworkLoad.this); + log.debug("Send PreliminaryDataRequest to " + connection + " succeeded."); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + gate.proceed(); + log.error( + "Sending PreliminaryDataRequest failed. That is expected if the peer is offline.\n\tException=" + + throwable.getMessage()); + } + }); + + } catch (Exception e) { + gate.proceed(); // release the gate on error + e.printStackTrace(); + } + }, current)); + } + + gate.engage(threadList.size()); + + // start all threads and wait until they all finished. We do that so we can + // minimize the time between querying the hosts and therefore the chance of + // inconsistencies. + threadList.forEach(Thread::start); + gate.await(); + + // report + Map report = new HashMap<>(); + // - assemble histograms + bucketsPerHost.forEach((host, buckets) -> buckets.forEach((type, counter) -> report + .put(OnionParser.prettyPrint(host) + "." + type, String.valueOf(counter.value())))); + + // - assemble diffs + Map messagesPerHost = new HashMap<>(); + bucketsPerHost.forEach((host, buckets) -> messagesPerHost.put(OnionParser.prettyPrint(host), + buckets.values().stream().mapToInt(Counter::value).sum())); + Optional referenceHost = messagesPerHost.keySet().stream().sorted().findFirst(); + Integer referenceValue = messagesPerHost.get(referenceHost.get()); + + messagesPerHost.forEach( + (host, numberOfMessages) -> { + try { + report.put(OnionParser.prettyPrint(host) + ".relativeNumberOfMessages", + String.valueOf(numberOfMessages - referenceValue)); + report.put(OnionParser.prettyPrint(host) + ".referenceHost", referenceHost.get()); + report.put(OnionParser.prettyPrint(host) + ".referenceValue", String.valueOf(referenceValue)); + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }); + + // when our hash cache exceeds a hard limit, we clear the cache and start anew + if (hashes.size() > 150000) + hashes.clear(); + + // in case we just started anew, do not report our findings as they contain not + // only the changes since our last run, but a whole lot more data dating back even + // to the beginning of bisq. + if (!hashes.isEmpty()) + reporter.report(report, "bisq." + getName()); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetDataResponse) { + + + GetDataResponse dataResponse = (GetDataResponse) networkEnvelope; + Map buckets = new HashMap<>(); + final Set dataSet = dataResponse.getDataSet(); + dataSet.forEach(e -> { + final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload(); + if (protectedStoragePayload == null) { + log.warn("StoragePayload was null: {}", networkEnvelope.toString()); + return; + } + + // memorize message hashes + hashes.add(P2PDataStorage.get32ByteHash(protectedStoragePayload)); + + // For logging different data types + String className = protectedStoragePayload.getClass().getSimpleName(); + try { + buckets.get(className).increment(); + } catch (NullPointerException nullPointerException) { + buckets.put(className, new Counter()); + } + }); + + Set persistableNetworkPayloadSet = dataResponse + .getPersistableNetworkPayloadSet(); + if (persistableNetworkPayloadSet != null) { + persistableNetworkPayloadSet.forEach(persistableNetworkPayload -> { + + // memorize message hashes + hashes.add(persistableNetworkPayload.getHash()); + + // For logging different data types + String className = persistableNetworkPayload.getClass().getSimpleName(); + buckets.putIfAbsent(className, new Counter()); + buckets.get(className).increment(); + }); + } + + checkNotNull(connection.peersNodeAddressProperty(), + "although the property is nullable, we need it to not be null"); + bucketsPerHost.put(connection.peersNodeAddressProperty().getValue(), buckets); + + connection.removeMessageListener(this); + gate.proceed(); + } else if (networkEnvelope instanceof CloseConnectionMessage) { + gate.unlock(); + } else { + log.warn("Got a message of type <{}>, expected ", + networkEnvelope.getClass().getSimpleName()); + } + } + + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + // open the gate + hsReady.proceed(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } +} diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkMessageSnapshot.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkMessageSnapshot.java new file mode 100644 index 0000000000..f367a5c9f7 --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkMessageSnapshot.java @@ -0,0 +1,247 @@ +/* + * 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.monitor.metric; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.monitor.AvailableTor; +import bisq.monitor.Metric; +import bisq.monitor.Monitor; +import bisq.monitor.OnionParser; +import bisq.monitor.Reporter; +import bisq.monitor.ThreadGate; +import bisq.network.p2p.CloseConnectionMessage; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.SetupListener; +import bisq.network.p2p.network.TorNetworkNode; +import bisq.network.p2p.peers.getdata.messages.GetDataResponse; +import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; +import lombok.extern.slf4j.Slf4j; + +/** + * Contacts a list of hosts and asks them for all the data excluding persisted messages. The + * answers are then compiled into buckets of message types. Based on these + * buckets, the Metric reports (for each host) the message types observed and + * their number. + * + * @author Florian Reimair + * + */ +@Slf4j +public class P2PNetworkMessageSnapshot extends Metric implements MessageListener, SetupListener { + + private static final String HOSTS = "run.hosts"; + private static final String TOR_PROXY_PORT = "run.torProxyPort"; + private NetworkNode networkNode; + private final File torHiddenServiceDir = new File("metric_p2pNetworkMessageStatus"); + private int nonce; + private Map> bucketsPerHost = new ConcurrentHashMap<>(); + private Set hashes = new HashSet<>(); + private final ThreadGate hsReady = new ThreadGate(); + private final ThreadGate gate = new ThreadGate(); + + /** + * Efficient way to count message occurrences. + */ + private class Counter { + private int value = 0; + + int value() { + return value; + } + + void increment() { + value++; + } + } + + public P2PNetworkMessageSnapshot(Reporter reporter) { + super(reporter); + + Version.setBaseCryptoNetworkId(0); // set to BTC_MAINNET + } + + @Override + protected void execute() { + // in case we do not have a NetworkNode up and running, we create one + if (null == networkNode) { + // prepare the gate + hsReady.engage(); + + // start the network node + networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9054")), + new CoreNetworkProtoResolver(), false, + new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName())); + networkNode.start(this); + + // wait for the HS to be published + hsReady.await(); + } + + // clear our buckets + bucketsPerHost.clear(); + ArrayList threadList = new ArrayList<>(); + + // for each configured host + for (String current : configuration.getProperty(HOSTS, "").split(",")) { + threadList.add(new Thread(() -> { + + try { + // parse Url + NodeAddress target = OnionParser.getNodeAddress(current); + + // do the data request + nonce = new Random().nextInt(); + SettableFuture future = networkNode.sendMessage(target, + new PreliminaryGetDataRequest(nonce, hashes)); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + connection.addMessageListener(P2PNetworkMessageSnapshot.this); + log.debug("Send PreliminaryDataRequest to " + connection + " succeeded."); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + gate.proceed(); + log.error( + "Sending PreliminaryDataRequest failed. That is expected if the peer is offline.\n\tException=" + + throwable.getMessage()); + } + }); + + } catch (Exception e) { + gate.proceed(); // release the gate on error + e.printStackTrace(); + } + + }, current)); + } + + gate.engage(threadList.size()); + + // start all threads and wait until they all finished. We do that so we can + // minimize the time between querying the hosts and therefore the chance of + // inconsistencies. + threadList.forEach(Thread::start); + gate.await(); + + // report + Map report = new HashMap<>(); + // - assemble histograms + bucketsPerHost.forEach((host, buckets) -> buckets.forEach((type, counter) -> report + .put(OnionParser.prettyPrint(host) + "." + type, String.valueOf(counter.value())))); + + // when our hash cache exceeds a hard limit, we clear the cache and start anew + if (hashes.size() > 150000) + hashes.clear(); + + // report our findings iff we have not just started anew + reporter.report(report, "bisq." + getName()); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetDataResponse) { + + + GetDataResponse dataResponse = (GetDataResponse) networkEnvelope; + Map buckets = new HashMap<>(); + final Set dataSet = dataResponse.getDataSet(); + dataSet.forEach(e -> { + final ProtectedStoragePayload protectedStoragePayload = e.getProtectedStoragePayload(); + if (protectedStoragePayload == null) { + log.warn("StoragePayload was null: {}", networkEnvelope.toString()); + return; + } + + // For logging different data types + String className = protectedStoragePayload.getClass().getSimpleName(); + + buckets.putIfAbsent(className, new Counter()); + buckets.get(className).increment(); + }); + + Set persistableNetworkPayloadSet = dataResponse + .getPersistableNetworkPayloadSet(); + if (persistableNetworkPayloadSet != null) { + persistableNetworkPayloadSet.forEach(persistableNetworkPayload -> { + + // memorize message hashes + hashes.add(persistableNetworkPayload.getHash()); + }); + } + + checkNotNull(connection.peersNodeAddressProperty(), + "although the property is nullable, we need it to not be null"); + bucketsPerHost.put(connection.peersNodeAddressProperty().getValue(), buckets); + + connection.removeMessageListener(this); + connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN); + gate.proceed(); + } else if (networkEnvelope instanceof CloseConnectionMessage) { + gate.unlock(); + } else { + log.warn("Got a message of type <{}>, expected ", + networkEnvelope.getClass().getSimpleName()); + connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN); + } + } + + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + // open the gate + hsReady.proceed(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } +} diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PRoundTripTime.java b/monitor/src/main/java/bisq/monitor/metric/P2PRoundTripTime.java new file mode 100644 index 0000000000..50915add5b --- /dev/null +++ b/monitor/src/main/java/bisq/monitor/metric/P2PRoundTripTime.java @@ -0,0 +1,185 @@ +/* + * 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.monitor.metric; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Random; + +import org.jetbrains.annotations.NotNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.core.proto.network.CoreNetworkProtoResolver; +import bisq.monitor.AvailableTor; +import bisq.monitor.Metric; +import bisq.monitor.Monitor; +import bisq.monitor.OnionParser; +import bisq.monitor.Reporter; +import bisq.monitor.StatisticsHelper; +import bisq.monitor.ThreadGate; +import bisq.network.p2p.CloseConnectionMessage; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.SetupListener; +import bisq.network.p2p.network.TorNetworkNode; +import bisq.network.p2p.peers.keepalive.messages.Ping; +import bisq.network.p2p.peers.keepalive.messages.Pong; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class P2PRoundTripTime extends Metric implements MessageListener, SetupListener { + + private static final String SAMPLE_SIZE = "run.sampleSize"; + private static final String HOSTS = "run.hosts"; + private static final String TOR_PROXY_PORT = "run.torProxyPort"; + private NetworkNode networkNode; + private final File torHiddenServiceDir = new File("metric_p2pRoundTripTime"); + private int nonce; + private long start; + private List samples; + private final ThreadGate gate = new ThreadGate(); + private final ThreadGate hsReady = new ThreadGate(); + + public P2PRoundTripTime(Reporter reporter) { + super(reporter); + + Version.setBaseCryptoNetworkId(0); // set to BTC_MAINNET + } + + @Override + public void configure(Properties properties) { + super.configure(properties); + } + + @Override + protected void execute() { + + if (null == networkNode) { + // close the gate + hsReady.engage(); + + networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9052")), + new CoreNetworkProtoResolver(), false, + new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName())); + networkNode.start(this); + + // wait for the gate to be reopened + hsReady.await(); + } + + + // for each configured host + for (String current : configuration.getProperty(HOSTS, "").split(",")) { + try { + // parse Url + NodeAddress target = OnionParser.getNodeAddress(current); + + // init sample bucket + samples = new ArrayList<>(); + + while (samples.size() < Integer.parseInt(configuration.getProperty(SAMPLE_SIZE, "1"))) { + // so we do not get disconnected due to DoS protection mechanisms + Thread.sleep(200); + + nonce = new Random().nextInt(); + + // close the gate + gate.engage(); + + start = System.currentTimeMillis(); + SettableFuture future = networkNode.sendMessage(target, new Ping(nonce, 42)); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + connection.addMessageListener(P2PRoundTripTime.this); + log.debug("Send ping to " + connection + " succeeded."); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + gate.proceed(); + log.error("Sending ping failed. That is expected if the peer is offline.\n\tException=" + + throwable.getMessage()); + } + }); + + // wait for the gate to open again + gate.await(); + } + + // report + reporter.report(StatisticsHelper.process(samples), + "bisq." + getName() + "." + OnionParser.prettyPrint(target)); + } catch (Exception e) { + gate.proceed(); // release the gate on error + e.printStackTrace(); + } + } + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof Pong) { + Pong pong = (Pong) networkEnvelope; + if (pong.getRequestNonce() == nonce) { + samples.add(System.currentTimeMillis() - start); + } else { + log.warn("Nonce not matching. That should never happen.\n\t" + + "We drop that message. nonce={} / requestNonce={}", + nonce, pong.getRequestNonce()); + } + connection.removeMessageListener(this); + + connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN); + // open the gate + gate.proceed(); + } else if (networkEnvelope instanceof CloseConnectionMessage) { + gate.unlock(); + } else { + log.warn("Got a message of type <{}>, expected ", networkEnvelope.getClass().getSimpleName()); + connection.shutDown(CloseConnectionReason.APP_SHUT_DOWN); + } + } + + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServicePublished() { + hsReady.proceed(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } +} diff --git a/monitor/src/main/java/bisq/monitor/metric/TorHiddenServiceStartupTime.java b/monitor/src/main/java/bisq/monitor/metric/TorHiddenServiceStartupTime.java index 39ba8bd75f..5ef02604df 100644 --- a/monitor/src/main/java/bisq/monitor/metric/TorHiddenServiceStartupTime.java +++ b/monitor/src/main/java/bisq/monitor/metric/TorHiddenServiceStartupTime.java @@ -19,6 +19,7 @@ package bisq.monitor.metric; import bisq.monitor.Metric; import bisq.monitor.Reporter; +import bisq.monitor.ThreadGate; import org.berndpruenster.netlayer.tor.HiddenServiceSocket; @@ -38,32 +39,12 @@ public class TorHiddenServiceStartupTime extends Metric { private static final String SERVICE_PORT = "run.servicePort"; private static final String LOCAL_PORT = "run.localPort"; private final String hiddenServiceDirectory = "metric_" + getName(); + private final ThreadGate gate = new ThreadGate(); public TorHiddenServiceStartupTime(Reporter reporter) { super(reporter); } - /** - * synchronization helper. Required because directly closing the - * HiddenServiceSocket in its ReadyListener causes a deadlock - */ - private void await() { - synchronized (hiddenServiceDirectory) { - try { - hiddenServiceDirectory.wait(); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - } - - private void proceed() { - synchronized (hiddenServiceDirectory) { - hiddenServiceDirectory.notify(); - } - } - @Override protected void execute() { // prepare settings. Fetch them every time we run the Metric so we do not have to @@ -75,6 +56,9 @@ public class TorHiddenServiceStartupTime extends Metric { new File(hiddenServiceDirectory).delete(); log.debug("creating the hidden service"); + + gate.engage(); + // start timer - we do not need System.nanoTime as we expect our result to be in // the range of tenth of seconds. long start = System.currentTimeMillis(); @@ -85,11 +69,11 @@ public class TorHiddenServiceStartupTime extends Metric { // stop the timer and report reporter.report(System.currentTimeMillis() - start, "bisq." + getName()); log.debug("the hidden service is ready"); - proceed(); + gate.proceed(); return null; }); - await(); + gate.await(); log.debug("going to revoke the hidden service..."); hiddenServiceSocket.close(); log.debug("[going to revoke the hidden service...] done"); diff --git a/monitor/src/main/java/bisq/monitor/metric/TorRoundTripTime.java b/monitor/src/main/java/bisq/monitor/metric/TorRoundTripTime.java index ab5f6da546..5616529ed9 100644 --- a/monitor/src/main/java/bisq/monitor/metric/TorRoundTripTime.java +++ b/monitor/src/main/java/bisq/monitor/metric/TorRoundTripTime.java @@ -18,7 +18,10 @@ package bisq.monitor.metric; import bisq.monitor.Metric; +import bisq.monitor.OnionParser; import bisq.monitor.Reporter; +import bisq.monitor.StatisticsHelper; +import bisq.network.p2p.NodeAddress; import org.berndpruenster.netlayer.tor.Tor; import org.berndpruenster.netlayer.tor.TorCtlException; @@ -26,17 +29,10 @@ import org.berndpruenster.netlayer.tor.TorCtlException; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import com.runjva.sourceforge.jsocks.protocol.SocksSocket; -import java.net.URL; - import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.LongSummaryStatistics; -import java.util.Map; - import static com.google.common.base.Preconditions.checkNotNull; /** @@ -65,7 +61,7 @@ public class TorRoundTripTime extends Metric { // for each configured host for (String current : configuration.getProperty(HOSTS, "").split(",")) { // parse Url - URL tmp = new URL(current); + NodeAddress tmp = OnionParser.getNodeAddress(current); List samples = new ArrayList<>(); @@ -75,7 +71,7 @@ public class TorRoundTripTime extends Metric { long start = System.currentTimeMillis(); // connect - socket = new SocksSocket(proxy, tmp.getHost(), tmp.getPort()); + socket = new SocksSocket(proxy, tmp.getHostName(), tmp.getPort()); // by the time we get here, we are connected samples.add(System.currentTimeMillis() - start); @@ -84,36 +80,8 @@ public class TorRoundTripTime extends Metric { socket.close(); } - // aftermath - Collections.sort(samples); - - // - average, max, min , sample size - LongSummaryStatistics statistics = samples.stream().mapToLong(val -> val).summaryStatistics(); - - Map results = new HashMap<>(); - results.put("average", String.valueOf(Math.round(statistics.getAverage()))); - results.put("max", String.valueOf(statistics.getMax())); - results.put("min", String.valueOf(statistics.getMin())); - results.put("sampleSize", String.valueOf(statistics.getCount())); - - // - p25, median, p75 - Integer[] percentiles = new Integer[]{25, 50, 75}; - for (Integer percentile : percentiles) { - double rank = statistics.getCount() * percentile / 100; - Long percentileValue; - if (samples.size() <= rank + 1) - percentileValue = samples.get(samples.size() - 1); - else if (Math.floor(rank) == rank) - percentileValue = samples.get((int) rank); - else - percentileValue = Math.round(samples.get((int) Math.floor(rank)) - + (samples.get((int) (Math.floor(rank) + 1)) - samples.get((int) Math.floor(rank))) - / (rank - Math.floor(rank))); - results.put("p" + percentile, String.valueOf(percentileValue)); - } - // report - reporter.report(results, "bisq." + getName()); + reporter.report(StatisticsHelper.process(samples), "bisq." + getName()); } } catch (TorCtlException | IOException e) { // TODO Auto-generated catch block diff --git a/monitor/src/main/java/bisq/monitor/reporter/GraphiteReporter.java b/monitor/src/main/java/bisq/monitor/reporter/GraphiteReporter.java index fd18f3af9c..322ddebb5a 100644 --- a/monitor/src/main/java/bisq/monitor/reporter/GraphiteReporter.java +++ b/monitor/src/main/java/bisq/monitor/reporter/GraphiteReporter.java @@ -17,14 +17,14 @@ package bisq.monitor.reporter; +import bisq.monitor.OnionParser; import bisq.monitor.Reporter; +import bisq.network.p2p.NodeAddress; import org.berndpruenster.netlayer.tor.TorSocket; -import java.net.URL; - import java.io.IOException; - +import java.net.Socket; import java.util.HashMap; import java.util.Map; @@ -55,13 +55,26 @@ public class GraphiteReporter extends Reporter { String report = prefix + ("".equals(key) ? "" : (prefix.isEmpty() ? "" : ".") + key) + " " + value + " " + timestamp + "\n"; - URL url; try { - url = new URL(configuration.getProperty("serviceUrl")); - TorSocket socket = new TorSocket(url.getHost(), url.getPort()); + NodeAddress nodeAddress = OnionParser.getNodeAddress(configuration.getProperty("serviceUrl")); + Socket socket; + if (nodeAddress.getFullAddress().contains(".onion")) + socket = new TorSocket(nodeAddress.getHostName(), nodeAddress.getPort()); + else + socket = new Socket(nodeAddress.getHostName(), nodeAddress.getPort()); socket.getOutputStream().write(report.getBytes()); socket.close(); + + try { + // give Tor some slack + // TODO maybe use the pickle protocol? + // https://graphite.readthedocs.io/en/latest/feeding-carbon.html + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); diff --git a/monitor/src/main/resources/logback.xml b/monitor/src/main/resources/logback.xml new file mode 100644 index 0000000000..65f307ab1f --- /dev/null +++ b/monitor/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/monitor/src/main/resources/metrics.properties b/monitor/src/main/resources/metrics.properties index 482796cd94..76314cda8f 100644 --- a/monitor/src/main/resources/metrics.properties +++ b/monitor/src/main/resources/metrics.properties @@ -24,6 +24,26 @@ TorHiddenServiceStartupTime.run.interval=100 TorHiddenServiceStartupTime.run.localPort=90501 TorHiddenServiceStartupTime.run.servicePort=90511 +#P2PRoundTripTime Metric +P2PRoundTripTime.enabled=true +P2PRoundTripTime.run.interval=100 +P2PRoundTripTime.run.sampleSize=5 +P2PRoundTripTime.run.hosts=723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 +P2PRoundTripTime.run.torProxyPort=9060 + +#P2PNetworkLoad Metric +P2PNetworkLoad.enabled=true +P2PNetworkLoad.run.interval=100 +P2PNetworkLoad.run.hosts=723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 +P2PNetworkLoad.run.torProxyPort=9061 + +#P2PNetworkMessageSnapshot Metric +P2PNetworkMessageSnapshot.enabled=true +P2PNetworkMessageSnapshot.run.interval=24 +P2PNetworkMessageSnapshot.run.hosts=3f3cu2yw7u457ztq.onion:8000, 723ljisnynbtdohi.onion:8000, fl3mmribyxgrv63c.onion:8000 +P2PNetworkMessageSnapshot.run.torProxyPort=9062 + + #Another Metric Another.run.interval=5 @@ -32,4 +52,4 @@ Another.run.interval=5 ## In contrast to Metrics, Reporters do not have a minimal set of properties. #GraphiteReporter -GraphiteReporter.serviceUrl=http://k6evlhg44acpchtc.onion:2003 +GraphiteReporter.serviceUrl=k6evlhg44acpchtc.onion:2003 diff --git a/monitor/src/test/java/bisq/monitor/P2PNetworkLoadTests.java b/monitor/src/test/java/bisq/monitor/P2PNetworkLoadTests.java new file mode 100644 index 0000000000..db24ea3bc9 --- /dev/null +++ b/monitor/src/test/java/bisq/monitor/P2PNetworkLoadTests.java @@ -0,0 +1,118 @@ +/* + * 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.monitor; + +import bisq.monitor.metric.P2PNetworkLoad; +import bisq.monitor.reporter.ConsoleReporter; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; +import java.util.Properties; + +import org.berndpruenster.netlayer.tor.NativeTor; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; +import org.junit.Assert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Test the round trip time metric against the hidden service of tor project.org. + * + * @author Florian Reimair + */ +@Disabled +class P2PNetworkLoadTests { + + /** + * A dummy Reporter for development purposes. + */ + private class DummyReporter extends ConsoleReporter { + + private Map results; + + @Override + public void report(long value) { + Assert.fail(); + } + + Map hasResults() { + return results; + } + + @Override + public void report(Map values) { + Assert.fail(); + } + + @Override + public void report(long value, String prefix) { + Assert.fail(); + } + + @Override + public void report(Map values, String prefix) { + super.report(values, prefix); + results = values; + } + } + + @BeforeAll + static void setup() throws TorCtlException { + // simulate the tor instance available to all metrics + Tor.setDefault(new NativeTor(Monitor.TOR_WORKING_DIR)); + } + + @Test + void run() throws Exception { + DummyReporter reporter = new DummyReporter(); + + // configure + Properties configuration = new Properties(); + configuration.put("P2PNetworkLoad.enabled", "true"); + configuration.put("P2PNetworkLoad.run.interval", "10"); + configuration.put("P2PNetworkLoad.run.hosts", + "http://fl3mmribyxgrv63c.onion:8000, http://3f3cu2yw7u457ztq.onion:8000"); + + Metric DUT = new P2PNetworkLoad(reporter); + // start + DUT.configure(configuration); + + // give it some time to start and then stop + while (!DUT.enabled()) + Thread.sleep(500); + Thread.sleep(20000); + + DUT.shutdown(); + DUT.join(); + + // observe results + Map results = reporter.hasResults(); + Assert.assertFalse(results.isEmpty()); + } + + @AfterAll + static void cleanup() { + Tor tor = Tor.getDefault(); + checkNotNull(tor, "tor must not be null"); + tor.shutdown(); + } +} diff --git a/monitor/src/test/java/bisq/monitor/P2PRoundTripTimeTests.java b/monitor/src/test/java/bisq/monitor/P2PRoundTripTimeTests.java new file mode 100644 index 0000000000..6c95b7f0cd --- /dev/null +++ b/monitor/src/test/java/bisq/monitor/P2PRoundTripTimeTests.java @@ -0,0 +1,136 @@ +/* + * 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.monitor; + +import bisq.monitor.metric.P2PRoundTripTime; +import bisq.monitor.reporter.ConsoleReporter; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; +import java.util.Properties; + +import org.berndpruenster.netlayer.tor.NativeTor; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; +import org.junit.Assert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Test the round trip time metric against the hidden service of tor project.org. + * + * @author Florian Reimair + */ +@Disabled +class P2PRoundTripTimeTests { + + /** + * A dummy Reporter for development purposes. + */ + private class DummyReporter extends ConsoleReporter { + + private Map results; + + @Override + public void report(long value) { + Assert.fail(); + } + + Map hasResults() { + return results; + } + + @Override + public void report(Map values) { + Assert.fail(); + } + + @Override + public void report(long value, String prefix) { + Assert.fail(); + } + + @Override + public void report(Map values, String prefix) { + super.report(values, prefix); + results = values; + } + } + + @BeforeAll + static void setup() throws TorCtlException { + // simulate the tor instance available to all metrics + Tor.setDefault(new NativeTor(Monitor.TOR_WORKING_DIR)); + } + + @ParameterizedTest + @ValueSource(strings = {"default", "3", "4", "10"}) + void run(String sampleSize) throws Exception { + DummyReporter reporter = new DummyReporter(); + + // configure + Properties configuration = new Properties(); + configuration.put("P2PRoundTripTime.enabled", "true"); + configuration.put("P2PRoundTripTime.run.interval", "2"); + if (!"default".equals(sampleSize)) + configuration.put("P2PRoundTripTime.run.sampleSize", sampleSize); + // torproject.org hidden service + configuration.put("P2PRoundTripTime.run.hosts", "http://fl3mmribyxgrv63c.onion:8000"); + configuration.put("P2PRoundTripTime.run.torProxyPort", "9052"); + + Metric DUT = new P2PRoundTripTime(reporter); + // start + DUT.configure(configuration); + + // give it some time to start and then stop + while (!DUT.enabled()) + Thread.sleep(2000); + + DUT.shutdown(); + DUT.join(); + + // observe results + Map results = reporter.hasResults(); + Assert.assertFalse(results.isEmpty()); + Assert.assertEquals(results.get("sampleSize"), sampleSize.equals("default") ? "1" : sampleSize); + + Integer p25 = Integer.valueOf(results.get("p25")); + Integer p50 = Integer.valueOf(results.get("p50")); + Integer p75 = Integer.valueOf(results.get("p75")); + Integer min = Integer.valueOf(results.get("min")); + Integer max = Integer.valueOf(results.get("max")); + Integer average = Integer.valueOf(results.get("average")); + + Assert.assertTrue(0 < min); + Assert.assertTrue(min <= p25 && p25 <= p50); + Assert.assertTrue(p50 <= p75); + Assert.assertTrue(p75 <= max); + Assert.assertTrue(min <= average && average <= max); + } + + @AfterAll + static void cleanup() { + Tor tor = Tor.getDefault(); + checkNotNull(tor, "tor must not be null"); + tor.shutdown(); + } +} diff --git a/scripts/install_java.sh b/scripts/install_java.sh old mode 100644 new mode 100755