mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-23 15:00:30 +01:00
Merge branch 'master' into dao-add-params
This commit is contained in:
commit
69a9f6f311
73 changed files with 2454 additions and 401 deletions
|
@ -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"
|
||||
|
|
107
common/src/main/java/bisq/common/util/PermutationUtil.java
Normal file
107
common/src/main/java/bisq/common/util/PermutationUtil.java
Normal 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;
|
||||
}
|
||||
}
|
432
common/src/test/java/bisq/common/util/PermutationTest.java
Normal file
432
common/src/test/java/bisq/common/util/PermutationTest.java
Normal 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()));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
69
core/src/main/java/bisq/core/app/TorSetup.java
Normal file
69
core/src/main/java/bisq/core/app/TorSetup.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"),
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,27 +68,31 @@ 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");
|
||||
hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed()
|
||||
.thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash())));
|
||||
|
||||
// If there are conflicting data views (multiple hashes) we only consider the voting round as valid if
|
||||
// the majority is a super majority with > 80%.
|
||||
if (hashWithStakeList.size() > 1) {
|
||||
long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum();
|
||||
long stakeOfFirst = hashWithStakeList.get(0).getStake();
|
||||
if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) {
|
||||
throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " +
|
||||
"total stake of all data views. We consider the voting cycle as invalid if the " +
|
||||
"winning data view does not reach a super majority.");
|
||||
}
|
||||
}
|
||||
return hashWithStakeList.get(0).getHash();
|
||||
} catch (Throwable t) {
|
||||
throw new VoteResultException.ValidationException(t);
|
||||
}
|
||||
|
||||
hashWithStakeList.sort(Comparator.comparingLong(VoteResultService.HashWithStake::getStake).reversed()
|
||||
.thenComparing(hashWithStake -> Utilities.encodeToHex(hashWithStake.getHash())));
|
||||
|
||||
// If there are conflicting data views (multiple hashes) we only consider the voting round as valid if
|
||||
// the majority is a super majority with > 80%.
|
||||
if (hashWithStakeList.size() > 1) {
|
||||
long stakeOfAll = hashWithStakeList.stream().mapToLong(VoteResultService.HashWithStake::getStake).sum();
|
||||
long stakeOfFirst = hashWithStakeList.get(0).getStake();
|
||||
if ((double) stakeOfFirst / (double) stakeOfAll < 0.8) {
|
||||
log.warn("The winning data view has less then 80% of the " +
|
||||
"total stake of all data views. We consider the voting cycle as invalid if the " +
|
||||
"winning data view does not reach a super majority. hashWithStakeList={}", hashWithStakeList);
|
||||
throw new VoteResultException.ConsensusException("The winning data view has less then 80% of the " +
|
||||
"total stake of all data views. We consider the voting cycle as invalid if the " +
|
||||
"winning data view does not reach a super majority.");
|
||||
}
|
||||
}
|
||||
return hashWithStakeList.get(0).getHash();
|
||||
}
|
||||
|
||||
// Key is stored after version and type bytes and list of Blind votes. It has 16 bytes
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
// Just for additional resilience we republish our blind votes
|
||||
List<BlindVote> sortedBlindVoteListOfCycle = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService);
|
||||
rePublishBlindVotePayloadList(sortedBlindVoteListOfCycle);
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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}?
|
||||
|
||||
|
|
|
@ -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} اطمینان دارید؟
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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}?
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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}?
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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} ธุรกรรม?
|
||||
|
||||
|
|
|
@ -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}?
|
||||
|
||||
|
|
|
@ -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
0
desktop/package/linux/package.sh
Normal file → Executable file
0
desktop/package/linux/release.sh
Normal file → Executable file
0
desktop/package/linux/release.sh
Normal file → Executable 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";
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 (proposalDisplay != null)
|
||||
proposalDisplay.clearForm();
|
||||
proposalTypeComboBox.getSelectionModel().clearSelection();
|
||||
if (!DevEnv.isDevMode())
|
||||
new Popup<>().confirmation(Res.get("dao.tx.published.success")).show();
|
||||
new Popup<>().feedback(Res.get("dao.tx.published.success")).show();
|
||||
},
|
||||
errorMessage -> new Popup<>().warning(errorMessage).show());
|
||||
|
||||
// We reset UI without waiting for callback as callback might be slow and then the user could create multiple
|
||||
// proposals.
|
||||
if (proposalDisplay != null)
|
||||
proposalDisplay.clearForm();
|
||||
proposalTypeComboBox.getSelectionModel().clearSelection();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -253,18 +261,20 @@ public class MakeProposalView extends ActivatableView<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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -22,8 +22,11 @@ import bisq.desktop.common.view.FxmlView;
|
|||
import bisq.desktop.components.BsqAddressTextField;
|
||||
import bisq.desktop.components.TitledGroupBg;
|
||||
import bisq.desktop.main.dao.wallet.BsqBalanceUtil;
|
||||
import bisq.desktop.util.FormBuilder;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
import bisq.desktop.util.Layout;
|
||||
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.btc.wallet.BsqWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.util.BsqFormatter;
|
||||
|
@ -32,6 +35,7 @@ import bisq.common.util.Tuple3;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
@ -61,28 +65,62 @@ public class BsqReceiveView extends ActivatableView<GridPane, Void> {
|
|||
|
||||
@Override
|
||||
public void initialize() {
|
||||
gridRow = bsqBalanceUtil.addGroup(root, gridRow);
|
||||
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) {
|
||||
gridRow = bsqBalanceUtil.addGroup(root, gridRow);
|
||||
|
||||
TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 1,
|
||||
Res.get("dao.wallet.receive.fundYourWallet"), Layout.GROUP_DISTANCE);
|
||||
GridPane.setColumnSpan(titledGroupBg, 3);
|
||||
Tuple3<Label, BsqAddressTextField, VBox> tuple = addLabelBsqAddressTextField(root, gridRow,
|
||||
Res.get("dao.wallet.receive.bsqAddress"),
|
||||
Layout.FIRST_ROW_AND_GROUP_DISTANCE);
|
||||
addressTextField = tuple.second;
|
||||
GridPane.setColumnSpan(tuple.third, 3);
|
||||
TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 1,
|
||||
Res.get("dao.wallet.receive.fundYourWallet"), Layout.GROUP_DISTANCE);
|
||||
GridPane.setColumnSpan(titledGroupBg, 3);
|
||||
Tuple3<Label, BsqAddressTextField, VBox> tuple = addLabelBsqAddressTextField(root, gridRow,
|
||||
Res.get("dao.wallet.receive.bsqAddress"),
|
||||
Layout.FIRST_ROW_AND_GROUP_DISTANCE);
|
||||
addressTextField = tuple.second;
|
||||
GridPane.setColumnSpan(tuple.third, 3);
|
||||
} else {
|
||||
addTitledGroupBg(root, gridRow, 6,
|
||||
Res.get("dao.wallet.receive.dao.headline"), 0);
|
||||
FormBuilder.addMultilineLabel(root, gridRow, Res.get("dao.wallet.receive.daoInfo"), 10);
|
||||
|
||||
Button daoInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoInfo.button"));
|
||||
daoInfoButton.setOnAction(e -> {
|
||||
GUIUtil.openWebPage("https://bisq.network/dao");
|
||||
});
|
||||
|
||||
FormBuilder.addMultilineLabel(root, ++gridRow, Res.get("dao.wallet.receive.daoTestnetInfo"));
|
||||
Button daoContributorInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoTestnetInfo.button"));
|
||||
daoContributorInfoButton.setOnAction(e -> {
|
||||
GUIUtil.openWebPage("https://bisq.network/dao-testnet");
|
||||
});
|
||||
|
||||
FormBuilder.addMultilineLabel(root, ++gridRow, Res.get("dao.wallet.receive.daoContributorInfo"));
|
||||
|
||||
Button daoTestnetInfoButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.wallet.receive.daoContributorInfo.button"));
|
||||
daoTestnetInfoButton.setOnAction(e -> {
|
||||
GUIUtil.openWebPage("https://bisq.network/dao-genesis");
|
||||
});
|
||||
|
||||
addTitledGroupBg(root, ++gridRow, 1,
|
||||
Res.get("dao.wallet.receive.fundYourWallet"), 20);
|
||||
Tuple3<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() {
|
||||
bsqBalanceUtil.activate();
|
||||
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq())
|
||||
bsqBalanceUtil.activate();
|
||||
|
||||
addressTextField.setAddress(bsqFormatter.getBsqAddressStringFromAddress(bsqWalletService.getUnusedAddress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
bsqBalanceUtil.deactivate();
|
||||
if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq())
|
||||
bsqBalanceUtil.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
50
monitor/src/main/java/bisq/monitor/AvailableTor.java
Normal file
50
monitor/src/main/java/bisq/monitor/AvailableTor.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
47
monitor/src/main/java/bisq/monitor/OnionParser.java
Normal file
47
monitor/src/main/java/bisq/monitor/OnionParser.java
Normal 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));
|
||||
}
|
||||
}
|
66
monitor/src/main/java/bisq/monitor/StatisticsHelper.java
Normal file
66
monitor/src/main/java/bisq/monitor/StatisticsHelper.java
Normal 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;
|
||||
}
|
||||
}
|
81
monitor/src/main/java/bisq/monitor/ThreadGate.java
Normal file
81
monitor/src/main/java/bisq/monitor/ThreadGate.java
Normal 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();
|
||||
}
|
||||
}
|
279
monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java
Normal file
279
monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
185
monitor/src/main/java/bisq/monitor/metric/P2PRoundTripTime.java
Normal file
185
monitor/src/main/java/bisq/monitor/metric/P2PRoundTripTime.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
12
monitor/src/main/resources/logback.xml
Normal file
12
monitor/src/main/resources/logback.xml
Normal 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>
|
|
@ -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
|
||||
|
|
118
monitor/src/test/java/bisq/monitor/P2PNetworkLoadTests.java
Normal file
118
monitor/src/test/java/bisq/monitor/P2PNetworkLoadTests.java
Normal 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();
|
||||
}
|
||||
}
|
136
monitor/src/test/java/bisq/monitor/P2PRoundTripTimeTests.java
Normal file
136
monitor/src/test/java/bisq/monitor/P2PRoundTripTimeTests.java
Normal 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
0
scripts/install_java.sh
Normal file → Executable file
Loading…
Add table
Reference in a new issue