Merge branch 'master' into dao-add-params

This commit is contained in:
Manfred Karrer 2019-02-03 21:13:48 +01:00
commit 69a9f6f311
No known key found for this signature in database
GPG key ID: 401250966A6B2C46
73 changed files with 2454 additions and 401 deletions

View file

@ -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"

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <T> Type of List items
* @return Partial list where items at indices of indicesToRemove have been removed
*/
public static <T> List<T> getPartialList(List<T> list, List<Integer> indicesToRemove) {
List<T> 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 <T> Type of list items
* @return List of possible permutations of the original list
*/
public static <T> List<List<T>> findAllPermutations(List<T> list, int maxIterations) {
List<List<T>> 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<List<T>> 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<T> 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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> list = new ArrayList<>(Arrays.asList(blindVote0, blindVote1, blindVote2, blindVote3, blindVote4, blindVote5));
List<Integer> indicesToRemove = Arrays.asList(0, 3);
List<String> expected = new ArrayList<>(Arrays.asList(blindVote1, blindVote2, blindVote4, blindVote5));
List<String> 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<String> list;
List<List<String>> expected;
List<List<String>> result;
List<String> 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()));
}
}

View file

@ -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);

View file

@ -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() {

View file

@ -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();

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}
}

View file

@ -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<PersistedDataHost> 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();

View file

@ -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;

View file

@ -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);

View file

@ -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"),

View file

@ -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;

View file

@ -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(),

View file

@ -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();
}

View file

@ -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<ProposalPayload> 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();
}
}
}
@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<Proposal> getValidatedProposals() {
return proposalPayloads.stream()
.map(ProposalPayload::getProposal)

View file

@ -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<VoteResultException> 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<VoteResultException>) 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<ProposalPayload> 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<BlindVotePayload> 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);
});
}
}

View file

@ -68,9 +68,13 @@ public class VoteResultConsensus {
// hex encoded hashOfProposalList for comparision
@Nullable
public static byte[] getMajorityHash(List<VoteResultService.HashWithStake> hashWithStakeList)
throws VoteResultException.ConsensusException, VoteResultException.ValidationException {
throws VoteResultException.ValidationException, VoteResultException.ConsensusException {
try {
checkArgument(!hashWithStakeList.isEmpty(), "hashWithStakeList must not be empty");
} catch (Throwable t) {
throw new VoteResultException.ValidationException(t);
}
hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed()
.thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash())));
@ -80,15 +84,15 @@ public class VoteResultConsensus {
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();
} catch (Throwable t) {
throw new VoteResultException.ValidationException(t);
}
}
// Key is stored after version and type bytes and list of Blind votes. It has 16 bytes

View file

@ -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);
}

View file

@ -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<DecryptedBallotsWithMerits> 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<BlindVote> permutatedListMatchingMajority = findPermutatedListMatchingMajority(majorityVoteListHash);
if (!permutatedListMatchingMajority.isEmpty()) {
log.info("We found a permutation of our blindVote list which matches the majority view. " +
"permutatedListMatchingMajority={}", permutatedListMatchingMajority);
Optional<List<BlindVote>> 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<BlindVote> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) {
private Optional<List<BlindVote>> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) {
List<BlindVote> 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<List<BlindVote>> result = PermutationUtil.findAllPermutations(list, 1000000);
for (List<BlindVote> 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<BlindVote> 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<EvaluatedProposal> getEvaluatedProposals(Set<DecryptedBallotsWithMerits> decryptedBallotsWithMeritsSet, int chainHeight) {
// We reorganize the data structure to have a map of proposals with a list of VoteWithStake objects
Map<Proposal, List<VoteWithStake>> resultListByProposalMap = getVoteWithStakeListByProposalMap(decryptedBallotsWithMeritsSet);

View file

@ -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<VoteRevealException> voteRevealExceptions = FXCollections.observableArrayList();
private final BsqNode bsqNode;
private final List<VoteRevealTxPublishedListener> 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<BlindVote> 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<BlindVote> 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());
if (inBlindVotePhase) {
// Just for additional resilience we republish our blind votes
List<BlindVote> 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

View file

@ -63,7 +63,11 @@ public abstract class BsqNode implements DaoSetupService {
@Nullable
protected Consumer<String> warnMessageHandler;
protected List<RawBlock> 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;

View file

@ -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<BlindVotePayload> 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<ProposalPayload> 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();
}
}
}

View file

@ -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.

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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"));
}
}
}

View file

@ -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

View file

@ -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?

View file

@ -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?

View file

@ -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}?

View file

@ -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} اطمینان دارید؟

View file

@ -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?

View file

@ -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?

View file

@ -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}?

View file

@ -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?

View file

@ -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}?

View file

@ -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?

View file

@ -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} ธุรกรรม?

View file

@ -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}?

View file

@ -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?

0
desktop/package/linux/package.sh Normal file → Executable file
View file

0
desktop/package/linux/release.sh Normal file → Executable file
View file

View file

@ -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";

View file

@ -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<StackPane, MainViewModel> {
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<StackPane, MainViewModel> {
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);

View file

@ -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 -> {

View file

@ -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<TabPane, Activatable> {
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<TabPane, Activatable> {
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 -> {

View file

@ -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());
}

View file

@ -173,7 +173,7 @@ public class ProofOfBurnView extends ActivatableView<GridPane, Void> 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);
}

View file

@ -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<SeparatedPhaseBars.SeparatedPhaseBarsItem> 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();

View file

@ -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<Label, HyperlinkWithIcon, VBox> 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());

View file

@ -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<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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));

View file

@ -215,9 +215,14 @@ public class MakeProposalView extends ActivatableView<GridPane, Void> 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<GridPane, Void> implements
daoFacade.publishMyProposal(proposal,
transaction,
() -> {
if (!DevEnv.isDevMode())
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();
if (!DevEnv.isDevMode())
new Popup<>().confirmation(Res.get("dao.tx.published.success")).show();
},
errorMessage -> new Popup<>().warning(errorMessage).show());
}
@Nullable
@ -253,18 +261,20 @@ public class MakeProposalView extends ActivatableView<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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());
}
}

View file

@ -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<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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<GridPane, Void> 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);

View file

@ -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<GridPane, Void> implements D
createCyclesTable();
}
@Override
protected void activate() {
super.activate();
@ -203,6 +206,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> 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<GridPane, Void> implements D
}
}
private void maybeShowVoteResultErrors(Cycle cycle) {
List<VoteResultException> 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<Ballot> 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<GridPane, Void> 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<GridPane, Void> implements D
long merit = meritAndStakeTuple.first;
long stake = meritAndStakeTuple.second;
proposalDisplay.applyBallotAndVoteWeight(ballot, merit, stake);
return proposalDisplay;
}

View file

@ -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,6 +65,7 @@ public class BsqReceiveView extends ActivatableView<GridPane, Void> {
@Override
public void initialize() {
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) {
gridRow = bsqBalanceUtil.addGroup(root, gridRow);
TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 1,
@ -71,10 +76,42 @@ public class BsqReceiveView extends ActivatableView<GridPane, Void> {
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<Label, BsqAddressTextField, VBox> tuple = addLabelBsqAddressTextField(root, gridRow,
Res.get("dao.wallet.receive.bsqAddress"),
40);
addressTextField = tuple.second;
GridPane.setColumnSpan(tuple.third, 3);
}
}
@Override
protected void activate() {
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq())
bsqBalanceUtil.activate();
addressTextField.setAddress(bsqFormatter.getBsqAddressStringFromAddress(bsqWalletService.getUnusedAddress()));
@ -82,6 +119,7 @@ public class BsqReceiveView extends ActivatableView<GridPane, Void> {
@Override
protected void deactivate() {
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq())
bsqBalanceUtil.deactivate();
}
}

View file

@ -244,6 +244,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
txSize,
receiversAddressInputTextField.getText(),
bsqFormatter,
btcFormatter,
() -> {
receiversAddressInputTextField.setText("");
amountInputTextField.setText("");
@ -298,6 +299,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
miningFee,
txSize, receiversBtcAddressInputTextField.getText(),
btcFormatter,
btcFormatter,
() -> {
receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText("");
@ -330,16 +332,17 @@ public class BsqSendView extends ActivatableView<GridPane, Void> 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() {

View file

@ -343,6 +343,7 @@ public abstract class Overlay<T extends 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;

View file

@ -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<TorNetworkSettingsWindow>
}
private final Preferences preferences;
private NetworkNode networkNode;
private final File torDir;
private final NetworkNode networkNode;
private final TorSetup torSetup;
private Label enterBridgeLabel;
private ComboBox<Transport> transportTypeComboBox;
private TextArea bridgeEntriesTextArea;
@ -106,10 +100,10 @@ public class TorNetworkSettingsWindow extends Overlay<TorNetworkSettingsWindow>
@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<TorNetworkSettingsWindow>
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);
});
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <code>Tor.getDefault()</code>
*
* @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;
}
}

View file

@ -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();
}

View file

@ -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();

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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));
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> process(List<Long> samples) {
// aftermath
Collections.sort(samples);
// - average, max, min , sample size
LongSummaryStatistics statistics = samples.stream().mapToLong(val -> val).summaryStatistics();
Map<String, String> 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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<NodeAddress, Map<String, Counter>> bucketsPerHost = new ConcurrentHashMap<>();
private Set<byte[]> 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<Thread> 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<Connection> 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<String, String> 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<String, Integer> messagesPerHost = new HashMap<>();
bucketsPerHost.forEach((host, buckets) -> messagesPerHost.put(OnionParser.prettyPrint(host),
buckets.values().stream().mapToInt(Counter::value).sum()));
Optional<String> 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<String, Counter> buckets = new HashMap<>();
final Set<ProtectedStorageEntry> 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<PersistableNetworkPayload> 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 <GetDataResponse>",
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() {
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<NodeAddress, Map<String, Counter>> bucketsPerHost = new ConcurrentHashMap<>();
private Set<byte[]> 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<Thread> 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<Connection> 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<String, String> 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<String, Counter> buckets = new HashMap<>();
final Set<ProtectedStorageEntry> 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<PersistableNetworkPayload> 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 <GetDataResponse>",
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() {
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Long> 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<Connection> 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 <Pong>", 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() {
}
}

View file

@ -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");

View file

@ -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<Long> 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<String, String> 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

View file

@ -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();

View file

@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<logger name="org.berndpruenster.netlayer.tor" level="INFO"/>
</configuration>

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
Assert.fail();
}
@Override
public void report(long value, String prefix) {
Assert.fail();
}
@Override
public void report(Map<String, String> 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<String, String> results = reporter.hasResults();
Assert.assertFalse(results.isEmpty());
}
@AfterAll
static void cleanup() {
Tor tor = Tor.getDefault();
checkNotNull(tor, "tor must not be null");
tor.shutdown();
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> results;
@Override
public void report(long value) {
Assert.fail();
}
Map<String, String> hasResults() {
return results;
}
@Override
public void report(Map<String, String> values) {
Assert.fail();
}
@Override
public void report(long value, String prefix) {
Assert.fail();
}
@Override
public void report(Map<String, String> 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<String, String> 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();
}
}

0
scripts/install_java.sh Normal file → Executable file
View file