diff --git a/build.gradle b/build.gradle index 6007dd14475..9ec1f8e50c4 100644 --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,7 @@ configure([project(':cli'), project(':seednode'), project(':statsnode'), project(':pricenode'), + project(':inventory'), project(':apitest')]) { apply plugin: 'application' @@ -594,6 +595,21 @@ configure(project(':daemon')) { } } +configure(project(':inventory')) { + apply plugin: 'com.github.johnrengelman.shadow' + + mainClassName = 'bisq.inventory.InventoryMonitorMain' + + dependencies { + compile project(':core') + compile "com.google.guava:guava:$guavaVersion" + compile "com.sparkjava:spark-core:$sparkVersion" + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} + configure(project(':apitest')) { mainClassName = 'bisq.apitest.ApiTestMain' diff --git a/common/src/main/java/bisq/common/setup/CommonSetup.java b/common/src/main/java/bisq/common/setup/CommonSetup.java index df3280aae7f..f2fac170fce 100644 --- a/common/src/main/java/bisq/common/setup/CommonSetup.java +++ b/common/src/main/java/bisq/common/setup/CommonSetup.java @@ -102,13 +102,13 @@ protected static void setSystemProperties() { protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) { Signal.handle(new Signal("INT"), signal -> { - gracefulShutDownHandler.gracefulShutDown(() -> { - }); + UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { + })); }); Signal.handle(new Signal("TERM"), signal -> { - gracefulShutDownHandler.gracefulShutDown(() -> { - }); + UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> { + })); }); } diff --git a/common/src/main/java/bisq/common/util/Profiler.java b/common/src/main/java/bisq/common/util/Profiler.java index 8a60402df81..28150ae9ec6 100644 --- a/common/src/main/java/bisq/common/util/Profiler.java +++ b/common/src/main/java/bisq/common/util/Profiler.java @@ -32,9 +32,13 @@ public static void printSystemLoad() { } public static long getUsedMemoryInMB() { + return getUsedMemoryInBytes() / 1024 / 1024; + } + + public static long getUsedMemoryInBytes() { Runtime runtime = Runtime.getRuntime(); - long free = runtime.freeMemory() / 1024 / 1024; - long total = runtime.totalMemory() / 1024 / 1024; + long free = runtime.freeMemory(); + long total = runtime.totalMemory(); return total - free; } diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java index 426dcb162ba..a2b90f816e8 100644 --- a/common/src/main/java/bisq/common/util/Utilities.java +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -40,6 +40,8 @@ import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import java.text.DecimalFormat; + import java.net.URI; import java.net.URISyntaxException; @@ -523,4 +525,11 @@ public static Predicate distinctByKey(Function keyExtr return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } + public static String readableFileSize(long size) { + if (size <= 0) return "0"; + String[] units = new String[]{"B", "kB", "MB", "GB", "TB"}; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.###").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } + } diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 3660544f60d..0f608b1a99e 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -260,7 +260,6 @@ public void start() { } private void step2() { - torSetup.cleanupTorFiles(); readMapsFromResources(this::step3); checkForCorrectOSArchitecture(); checkOSXVersion(); diff --git a/core/src/main/java/bisq/core/app/TorSetup.java b/core/src/main/java/bisq/core/app/TorSetup.java index b2ff5378b79..b855e514622 100644 --- a/core/src/main/java/bisq/core/app/TorSetup.java +++ b/core/src/main/java/bisq/core/app/TorSetup.java @@ -46,15 +46,7 @@ public TorSetup(@Named(Config.TOR_DIR) File torDir) { this.torDir = checkDir(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. + // Should only be called if needed. Slows down Tor startup from about 5 sec. to 30 sec. if it gets deleted. public void cleanupTorFiles(@Nullable Runnable resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()); try { diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java index 8b2dc545d6c..bad57eba85a 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2P.java @@ -73,7 +73,6 @@ public AppSetupWithP2P(P2PService p2PService, @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 diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index a528903c3d9..1c8edc478f4 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -216,13 +216,13 @@ public void setFilterWarningHandler(Consumer filterWarningHandler) { addListener(filter -> { if (filter != null && filterWarningHandler != null) { if (filter.getSeedNodes() != null && !filter.getSeedNodes().isEmpty()) { - log.info(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed"))); + log.info("One of the seed nodes got banned. {}", filter.getSeedNodes()); // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed"))); } if (filter.getPriceRelayNodes() != null && !filter.getPriceRelayNodes().isEmpty()) { - log.info(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay"))); + log.info("One of the price relay nodes got banned. {}", filter.getPriceRelayNodes()); // Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users. // filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay"))); } diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java new file mode 100644 index 00000000000..5211077ab24 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestHandler.java @@ -0,0 +1,176 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory; + +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.state.DaoStateService; +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.core.network.p2p.inventory.model.InventoryItem; + +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.Statistic; +import bisq.network.p2p.peers.PeerManager; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Profiler; +import bisq.common.util.Utilities; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.common.base.Enums; +import com.google.common.base.Optional; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; + +import java.lang.management.ManagementFactory; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GetInventoryRequestHandler implements MessageListener { + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final P2PDataStorage p2PDataStorage; + private final DaoStateService daoStateService; + private final DaoStateMonitoringService daoStateMonitoringService; + private final ProposalStateMonitoringService proposalStateMonitoringService; + private final BlindVoteStateMonitoringService blindVoteStateMonitoringService; + private final int maxConnections; + + @Inject + public GetInventoryRequestHandler(NetworkNode networkNode, + PeerManager peerManager, + P2PDataStorage p2PDataStorage, + DaoStateService daoStateService, + DaoStateMonitoringService daoStateMonitoringService, + ProposalStateMonitoringService proposalStateMonitoringService, + BlindVoteStateMonitoringService blindVoteStateMonitoringService, + @Named(Config.MAX_CONNECTIONS) int maxConnections) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.p2PDataStorage = p2PDataStorage; + this.daoStateService = daoStateService; + this.daoStateMonitoringService = daoStateMonitoringService; + this.proposalStateMonitoringService = proposalStateMonitoringService; + this.blindVoteStateMonitoringService = blindVoteStateMonitoringService; + this.maxConnections = maxConnections; + + this.networkNode.addMessageListener(this); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof GetInventoryRequest) { + // Data + GetInventoryRequest getInventoryRequest = (GetInventoryRequest) networkEnvelope; + Map dataObjects = new HashMap<>(); + p2PDataStorage.getMapForDataResponse(getInventoryRequest.getVersion()).values().stream() + .map(e -> e.getClass().getSimpleName()) + .forEach(className -> { + Optional optionalEnum = Enums.getIfPresent(InventoryItem.class, className); + if (optionalEnum.isPresent()) { + InventoryItem key = optionalEnum.get(); + dataObjects.putIfAbsent(key, 0); + int prev = dataObjects.get(key); + dataObjects.put(key, prev + 1); + } + }); + p2PDataStorage.getMap().values().stream() + .map(ProtectedStorageEntry::getProtectedStoragePayload) + .filter(Objects::nonNull) + .map(e -> e.getClass().getSimpleName()) + .forEach(className -> { + Optional optionalEnum = Enums.getIfPresent(InventoryItem.class, className); + if (optionalEnum.isPresent()) { + InventoryItem key = optionalEnum.get(); + dataObjects.putIfAbsent(key, 0); + int prev = dataObjects.get(key); + dataObjects.put(key, prev + 1); + } + }); + Map inventory = new HashMap<>(); + dataObjects.forEach((key, value) -> inventory.put(key, String.valueOf(value))); + + // DAO + int numBsqBlocks = daoStateService.getBlocks().size(); + inventory.put(InventoryItem.numBsqBlocks, String.valueOf(numBsqBlocks)); + + int daoStateChainHeight = daoStateService.getChainHeight(); + inventory.put(InventoryItem.daoStateChainHeight, String.valueOf(daoStateChainHeight)); + + LinkedList daoStateBlockChain = daoStateMonitoringService.getDaoStateBlockChain(); + if (!daoStateBlockChain.isEmpty()) { + String daoStateHash = Utilities.bytesAsHexString(daoStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.daoStateHash, daoStateHash); + } + + LinkedList proposalStateBlockChain = proposalStateMonitoringService.getProposalStateBlockChain(); + if (!proposalStateBlockChain.isEmpty()) { + String proposalHash = Utilities.bytesAsHexString(proposalStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.proposalHash, proposalHash); + } + + LinkedList blindVoteStateBlockChain = blindVoteStateMonitoringService.getBlindVoteStateBlockChain(); + if (!blindVoteStateBlockChain.isEmpty()) { + String blindVoteHash = Utilities.bytesAsHexString(blindVoteStateBlockChain.getLast().getMyStateHash().getHash()); + inventory.put(InventoryItem.blindVoteHash, blindVoteHash); + } + + // network + inventory.put(InventoryItem.maxConnections, String.valueOf(maxConnections)); + inventory.put(InventoryItem.numConnections, String.valueOf(networkNode.getAllConnections().size())); + inventory.put(InventoryItem.peakNumConnections, String.valueOf(peerManager.getPeakNumConnections())); + inventory.put(InventoryItem.numAllConnectionsLostEvents, String.valueOf(peerManager.getNumAllConnectionsLostEvents())); + inventory.put(InventoryItem.sentBytes, String.valueOf(Statistic.totalSentBytesProperty().get())); + inventory.put(InventoryItem.sentBytesPerSec, String.valueOf(Statistic.totalSentBytesPerSecProperty().get())); + inventory.put(InventoryItem.receivedBytes, String.valueOf(Statistic.totalReceivedBytesProperty().get())); + inventory.put(InventoryItem.receivedBytesPerSec, String.valueOf(Statistic.totalReceivedBytesPerSecProperty().get())); + inventory.put(InventoryItem.receivedMessagesPerSec, String.valueOf(Statistic.numTotalReceivedMessagesPerSecProperty().get())); + inventory.put(InventoryItem.sentMessagesPerSec, String.valueOf(Statistic.numTotalSentMessagesPerSecProperty().get())); + + // node + inventory.put(InventoryItem.version, Version.VERSION); + inventory.put(InventoryItem.usedMemory, String.valueOf(Profiler.getUsedMemoryInBytes())); + inventory.put(InventoryItem.jvmStartTime, String.valueOf(ManagementFactory.getRuntimeMXBean().getStartTime())); + + log.info("Send inventory {} to {}", inventory, connection.getPeersNodeAddressOptional()); + GetInventoryResponse getInventoryResponse = new GetInventoryResponse(inventory); + networkNode.sendMessage(connection, getInventoryResponse); + } + } + + public void shutDown() { + networkNode.removeMessageListener(this); + } +} diff --git a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestManager.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java similarity index 84% rename from p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestManager.java rename to core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java index 755f2912c03..816bb786fee 100644 --- a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestManager.java +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequestManager.java @@ -15,7 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.network.p2p.inventory; +package bisq.core.network.p2p.inventory; + +import bisq.core.network.p2p.inventory.model.InventoryItem; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.network.NetworkNode; @@ -41,11 +43,12 @@ public GetInventoryRequestManager(NetworkNode networkNode) { } public void request(NodeAddress nodeAddress, - Consumer> resultHandler, + Consumer> resultHandler, ErrorMessageHandler errorMessageHandler) { if (requesterMap.containsKey(nodeAddress)) { - log.warn("There is still an open request pending for {}", nodeAddress.getFullAddress()); - return; + log.warn("There was still a pending request for {}. We shut it down and make a new request", + nodeAddress.getFullAddress()); + requesterMap.get(nodeAddress).shutDown(); } GetInventoryRequester getInventoryRequester = new GetInventoryRequester(networkNode, diff --git a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequester.java b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java similarity index 61% rename from p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequester.java rename to core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java index b43f1d37893..a4961e824dc 100644 --- a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequester.java +++ b/core/src/main/java/bisq/core/network/p2p/inventory/GetInventoryRequester.java @@ -15,12 +15,16 @@ * along with Bisq. If not, see . */ -package bisq.network.p2p.inventory; +package bisq.core.network.p2p.inventory; + +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.core.network.p2p.inventory.model.InventoryItem; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.inventory.messages.GetInventoryRequest; -import bisq.network.p2p.inventory.messages.GetInventoryResponse; +import bisq.network.p2p.network.CloseConnectionReason; import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.ConnectionListener; import bisq.network.p2p.network.MessageListener; import bisq.network.p2p.network.NetworkNode; @@ -36,18 +40,18 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class GetInventoryRequester implements MessageListener { +public class GetInventoryRequester implements MessageListener, ConnectionListener { private final static int TIMEOUT_SEC = 90; private final NetworkNode networkNode; private final NodeAddress nodeAddress; - private final Consumer> resultHandler; + private final Consumer> resultHandler; private final ErrorMessageHandler errorMessageHandler; private Timer timer; public GetInventoryRequester(NetworkNode networkNode, NodeAddress nodeAddress, - Consumer> resultHandler, + Consumer> resultHandler, ErrorMessageHandler errorMessageHandler) { this.networkNode = networkNode; this.nodeAddress = nodeAddress; @@ -57,8 +61,12 @@ public GetInventoryRequester(NetworkNode networkNode, public void request() { networkNode.addMessageListener(this); + networkNode.addConnectionListener(this); + timer = UserThread.runAfter(this::onTimeOut, TIMEOUT_SEC); - networkNode.sendMessage(nodeAddress, new GetInventoryRequest(Version.VERSION)); + + GetInventoryRequest getInventoryRequest = new GetInventoryRequest(Version.VERSION); + networkNode.sendMessage(nodeAddress, getInventoryRequest); } private void onTimeOut() { @@ -72,8 +80,11 @@ public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { connection.getPeersNodeAddressOptional().ifPresent(peer -> { if (peer.equals(nodeAddress)) { GetInventoryResponse getInventoryResponse = (GetInventoryResponse) networkEnvelope; - resultHandler.accept(getInventoryResponse.getNumPayloadsMap()); + resultHandler.accept(getInventoryResponse.getInventory()); shutDown(); + + // We shut down our connection after work as our node is not helpful for the network. + UserThread.runAfter(() -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER), 1); } }); } @@ -85,5 +96,27 @@ public void shutDown() { timer = null; } networkNode.removeMessageListener(this); + networkNode.removeConnectionListener(this); + } + + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, + Connection connection) { + connection.getPeersNodeAddressOptional().ifPresent(address -> { + if (address.equals(nodeAddress)) { + if (!closeConnectionReason.isIntended) { + errorMessageHandler.handleErrorMessage("Connected closed because of " + closeConnectionReason.name()); + } + shutDown(); + } + }); + } + + @Override + public void onError(Throwable throwable) { } } diff --git a/p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryRequest.java b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java similarity index 97% rename from p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryRequest.java rename to core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java index dc26d377742..fee7f827040 100644 --- a/p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryRequest.java +++ b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryRequest.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.network.p2p.inventory.messages; +package bisq.core.network.p2p.inventory.messages; import bisq.common.app.Version; diff --git a/p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryResponse.java b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java similarity index 54% rename from p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryResponse.java rename to core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java index 45b5fbf9929..b9c5cf9ce34 100644 --- a/p2p/src/main/java/bisq/network/p2p/inventory/messages/GetInventoryResponse.java +++ b/core/src/main/java/bisq/core/network/p2p/inventory/messages/GetInventoryResponse.java @@ -15,42 +15,60 @@ * along with Bisq. If not, see . */ -package bisq.network.p2p.inventory.messages; +package bisq.core.network.p2p.inventory.messages; + +import bisq.core.network.p2p.inventory.model.InventoryItem; import bisq.common.app.Version; import bisq.common.proto.network.NetworkEnvelope; +import com.google.common.base.Enums; +import com.google.common.base.Optional; + +import java.util.HashMap; import java.util.Map; import lombok.Value; @Value public class GetInventoryResponse extends NetworkEnvelope { - private final Map numPayloadsMap; + private final Map inventory; - public GetInventoryResponse(Map numPayloadsMap) { - this(numPayloadsMap, Version.getP2PMessageVersion()); + public GetInventoryResponse(Map inventory) { + this(inventory, Version.getP2PMessageVersion()); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private GetInventoryResponse(Map numPayloadsMap, int messageVersion) { + private GetInventoryResponse(Map inventory, int messageVersion) { super(messageVersion); - this.numPayloadsMap = numPayloadsMap; + this.inventory = inventory; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + // For protobuf we use a map with a string key + Map map = new HashMap<>(); + inventory.forEach((key, value) -> map.put(key.getKey(), value)); return getNetworkEnvelopeBuilder() .setGetInventoryResponse(protobuf.GetInventoryResponse.newBuilder() - .putAllNumPayloadsMap(numPayloadsMap)) + .putAllInventory(map)) .build(); } public static GetInventoryResponse fromProto(protobuf.GetInventoryResponse proto, int messageVersion) { - return new GetInventoryResponse(proto.getNumPayloadsMapMap(), messageVersion); + // For protobuf we use a map with a string key + Map map = proto.getInventoryMap(); + Map inventory = new HashMap<>(); + map.forEach((key, value) -> { + Optional optional = Enums.getIfPresent(InventoryItem.class, key); + if (optional.isPresent()) { + inventory.put(optional.get(), value); + } + }); + return new GetInventoryResponse(inventory, messageVersion); } } diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java new file mode 100644 index 00000000000..3ed4a183103 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/Average.java @@ -0,0 +1,46 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class Average { + public static Map of(Set requestInfoSet) { + Map averageValuesPerItem = new HashMap<>(); + Arrays.asList(InventoryItem.values()).forEach(inventoryItem -> { + if (inventoryItem.isNumberValue()) { + averageValuesPerItem.put(inventoryItem, getAverage(requestInfoSet, inventoryItem)); + } + }); + return averageValuesPerItem; + } + + public static double getAverage(Set requestInfoSet, InventoryItem inventoryItem) { + return requestInfoSet.stream() + .map(RequestInfo::getInventory) + .filter(Objects::nonNull) + .filter(inventory -> inventory.containsKey(inventoryItem)) + .mapToDouble(inventory -> Double.parseDouble((inventory.get(inventoryItem)))) + .average() + .orElse(0d); + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java new file mode 100644 index 00000000000..f824df87179 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByIntegerDiff.java @@ -0,0 +1,83 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +import bisq.common.util.Tuple2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public class DeviationByIntegerDiff implements DeviationType { + private final int warnTrigger; + private final int alertTrigger; + + public DeviationByIntegerDiff(int warnTrigger, int alertTrigger) { + this.warnTrigger = warnTrigger; + this.alertTrigger = alertTrigger; + } + + public DeviationSeverity getDeviationSeverity(Collection> collection, + @Nullable String value, + InventoryItem inventoryItem) { + DeviationSeverity deviationSeverity = DeviationSeverity.OK; + if (value == null) { + return deviationSeverity; + } + + Map sameItemsByValue = new HashMap<>(); + collection.stream() + .filter(list -> !list.isEmpty()) + .map(list -> list.get(list.size() - 1)) // We use last item only + .map(RequestInfo::getInventory) + .filter(Objects::nonNull) + .map(e -> e.get(inventoryItem)) + .forEach(e -> { + sameItemsByValue.putIfAbsent(e, 0); + int prev = sameItemsByValue.get(e); + sameItemsByValue.put(e, prev + 1); + }); + if (sameItemsByValue.size() > 1) { + List> sameItems = new ArrayList<>(); + sameItemsByValue.forEach((k, v) -> sameItems.add(new Tuple2<>(k, v))); + sameItems.sort(Comparator.comparing(o -> o.second)); + Collections.reverse(sameItems); + String majority = sameItems.get(0).first; + if (!majority.equals(value)) { + int majorityAsInt = Integer.parseInt(majority); + int valueAsInt = Integer.parseInt(value); + int diff = Math.abs(majorityAsInt - valueAsInt); + if (diff >= alertTrigger) { + deviationSeverity = DeviationSeverity.ALERT; + } else if (diff >= warnTrigger) { + deviationSeverity = DeviationSeverity.WARN; + } else { + deviationSeverity = DeviationSeverity.OK; + } + } + } + return deviationSeverity; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java new file mode 100644 index 00000000000..60093d2e83a --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationByPercentage.java @@ -0,0 +1,47 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +public class DeviationByPercentage implements DeviationType { + private final double lowerAlertTrigger; + private final double upperAlertTrigger; + private final double lowerWarnTrigger; + private final double upperWarnTrigger; + + public DeviationByPercentage(double lowerAlertTrigger, + double upperAlertTrigger, + double lowerWarnTrigger, + double upperWarnTrigger) { + this.lowerAlertTrigger = lowerAlertTrigger; + this.upperAlertTrigger = upperAlertTrigger; + this.lowerWarnTrigger = lowerWarnTrigger; + this.upperWarnTrigger = upperWarnTrigger; + } + + public DeviationSeverity getDeviationSeverity(double deviation) { + if (deviation <= lowerAlertTrigger || deviation >= upperAlertTrigger) { + return DeviationSeverity.ALERT; + } + + if (deviation <= lowerWarnTrigger || deviation >= upperWarnTrigger) { + return DeviationSeverity.WARN; + } + + return DeviationSeverity.OK; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java new file mode 100644 index 00000000000..d1a9efcb307 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationOfHashes.java @@ -0,0 +1,75 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +import bisq.common.util.Tuple2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public class DeviationOfHashes implements DeviationType { + public DeviationSeverity getDeviationSeverity(Collection> collection, + @Nullable String value, + InventoryItem inventoryItem, + String currentBlockHeight) { + DeviationSeverity deviationSeverity = DeviationSeverity.OK; + if (value == null) { + return deviationSeverity; + } + + Map sameHashesPerHashListByHash = new HashMap<>(); + collection.stream() + .filter(list -> !list.isEmpty()) + .map(list -> list.get(list.size() - 1)) // We use last item only + .map(RequestInfo::getInventory) + .filter(Objects::nonNull) + .filter(inventory -> inventory.get(InventoryItem.daoStateChainHeight).equals(currentBlockHeight)) + .map(inventory -> inventory.get(inventoryItem)) + .forEach(v -> { + sameHashesPerHashListByHash.putIfAbsent(v, 0); + int prev = sameHashesPerHashListByHash.get(v); + sameHashesPerHashListByHash.put(v, prev + 1); + }); + if (sameHashesPerHashListByHash.size() > 1) { + List> sameHashesPerHashList = new ArrayList<>(); + sameHashesPerHashListByHash.forEach((k, v) -> sameHashesPerHashList.add(new Tuple2<>(k, v))); + sameHashesPerHashList.sort(Comparator.comparing(o -> o.second)); + Collections.reverse(sameHashesPerHashList); + + // It could be that first and any following list entry has same number of hashes, but we ignore that as + // it is reason enough to alert the operators in case not all hashes are the same. + if (sameHashesPerHashList.get(0).first.equals(value)) { + // We are in the majority group. + // We also set a warning to make sure the operators act quickly and to check if there are + // more severe issues. + deviationSeverity = DeviationSeverity.WARN; + } else { + deviationSeverity = DeviationSeverity.ALERT; + } + } + return deviationSeverity; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java new file mode 100644 index 00000000000..2c40937425c --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationSeverity.java @@ -0,0 +1,24 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +public enum DeviationSeverity { + OK, + WARN, + ALERT +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java new file mode 100644 index 00000000000..565292eee70 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/DeviationType.java @@ -0,0 +1,21 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +public interface DeviationType { +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java new file mode 100644 index 00000000000..a978727f5b4 --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/InventoryItem.java @@ -0,0 +1,178 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +import org.jetbrains.annotations.Nullable; + +public enum InventoryItem { + // Percentage deviation + OfferPayload("OfferPayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05)), + MailboxStoragePayload("MailboxStoragePayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05)), + TradeStatistics3("MailboxStoragePayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05)), + AccountAgeWitness("MailboxStoragePayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05)), + SignedWitness("MailboxStoragePayload", + true, + new DeviationByPercentage(0.9, 1.1, 0.95, 1.05)), + + // Should be same value + Alert("Alert", + true, + new DeviationByIntegerDiff(1, 1)), + Filter("Filter", + true, + new DeviationByIntegerDiff(1, 1)), + Mediator("Mediator", + true, + new DeviationByIntegerDiff(1, 1)), + RefundAgent("RefundAgent", + true, + new DeviationByIntegerDiff(1, 1)), + + // Should be very close values + TempProposalPayload("TempProposalPayload", + true, + new DeviationByIntegerDiff(3, 5)), + ProposalPayload("ProposalPayload", + true, + new DeviationByIntegerDiff(1, 2)), + BlindVotePayload("BlindVotePayload", + true, + new DeviationByIntegerDiff(1, 2)), + + // Should be very close values + daoStateChainHeight("daoStateChainHeight", + true, + new DeviationByIntegerDiff(1, 3)), + numBsqBlocks("numBsqBlocks", + true, + new DeviationByIntegerDiff(1, 3)), + + // Has to be same values at same block + daoStateHash("daoStateHash", + false, + new DeviationOfHashes()), + proposalHash("proposalHash", + false, + new DeviationOfHashes()), + blindVoteHash("blindVoteHash", + false, + new DeviationOfHashes()), + + // Percentage deviation + maxConnections("maxConnections", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + numConnections("numConnections", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + peakNumConnections("peakNumConnections", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + numAllConnectionsLostEvents("numAllConnectionsLostEvents", + true, + new DeviationByIntegerDiff(1, 2)), + sentBytesPerSec("sentBytesPerSec", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + receivedBytesPerSec("receivedBytesPerSec", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + receivedMessagesPerSec("receivedMessagesPerSec", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + sentMessagesPerSec("sentMessagesPerSec", + true, + new DeviationByPercentage(0.33, 3, 0.4, 2.5)), + + // No deviation check + sentBytes("sentBytes", true), + receivedBytes("receivedBytes", true), + + // No deviation check + version("version", false), + usedMemory("usedMemory", true), + jvmStartTime("jvmStartTime", true); + + @Getter + private final String key; + @Getter + private final boolean isNumberValue; + @Getter + @Nullable + private DeviationType deviationType; + + InventoryItem(String key, boolean isNumberValue) { + this.key = key; + this.isNumberValue = isNumberValue; + } + + InventoryItem(String key, boolean isNumberValue, DeviationType deviationType) { + this(key, isNumberValue); + this.deviationType = deviationType; + } + + @Nullable + public Double getDeviation(Map averageValues, @Nullable String value) { + if (averageValues.containsKey(this) && value != null) { + double averageValue = averageValues.get(this); + return getDeviation(value, averageValue); + } + return null; + } + + @Nullable + public Double getDeviation(@Nullable String value, double average) { + if (deviationType != null && value != null && average != 0 && isNumberValue) { + return Double.parseDouble(value) / average; + } + return null; + } + + public DeviationSeverity getDeviationSeverity(Double deviation, + Collection> collection, + @Nullable String value, + String currentBlockHeight) { + if (deviationType == null || deviation == null || value == null) { + return DeviationSeverity.OK; + } + + if (deviationType instanceof DeviationByPercentage) { + return ((DeviationByPercentage) deviationType).getDeviationSeverity(deviation); + } else if (deviationType instanceof DeviationByIntegerDiff) { + return ((DeviationByIntegerDiff) deviationType).getDeviationSeverity(collection, value, this); + } else if (deviationType instanceof DeviationOfHashes) { + return ((DeviationOfHashes) deviationType).getDeviationSeverity(collection, value, this, currentBlockHeight); + } else { + return DeviationSeverity.OK; + } + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java new file mode 100644 index 00000000000..c663dd449db --- /dev/null +++ b/core/src/main/java/bisq/core/network/p2p/inventory/model/RequestInfo.java @@ -0,0 +1,54 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p.inventory.model; + +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +import org.jetbrains.annotations.Nullable; + +@Getter +public class RequestInfo { + private final long requestStartTime; + @Setter + private long responseTime; + @Nullable + @Setter + private Map inventory; + @Nullable + @Setter + private String errorMessage; + + public RequestInfo(long requestStartTime) { + this.requestStartTime = requestStartTime; + } + + public String getDisplayValue(InventoryItem inventoryItem) { + String value = getValue(inventoryItem); + return value != null ? value : "n/a"; + } + + @Nullable + public String getValue(InventoryItem inventoryItem) { + return inventory != null && inventory.containsKey(inventoryItem) ? + inventory.get(inventoryItem) : + null; + } +} diff --git a/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java index ec4f70ef59a..4c52456fb6c 100644 --- a/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java +++ b/core/src/main/java/bisq/core/network/p2p/seed/DefaultSeedNodeRepository.java @@ -29,8 +29,11 @@ import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -62,23 +65,9 @@ private void reload() { return; } - // else, we fetch the seed nodes from our resources - InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream(config.baseCurrencyNetwork.name().toLowerCase() + ENDING); - BufferedReader seedNodeFile = new BufferedReader(new InputStreamReader(fileInputStream)); - - // only clear if we have a fresh data source (otherwise, an exception would prevent us from getting here) cache.clear(); - - // refill the cache - seedNodeFile.lines().forEach(line -> { - Matcher matcher = pattern.matcher(line); - if (matcher.find()) - cache.add(new NodeAddress(matcher.group(1))); - - // Maybe better include in regex... - if (line.startsWith("localhost")) - cache.add(new NodeAddress(line)); - }); + List result = getSeedNodeAddressesFromPropertyFile(config.baseCurrencyNetwork.name().toLowerCase()); + cache.addAll(result); // filter cache.removeAll( @@ -95,6 +84,34 @@ private void reload() { } } + public static Optional readSeedNodePropertyFile(String fileName) { + InputStream fileInputStream = DefaultSeedNodeRepository.class.getClassLoader().getResourceAsStream( + fileName + ENDING); + if (fileInputStream == null) { + return Optional.empty(); + } + return Optional.of(new BufferedReader(new InputStreamReader(fileInputStream))); + } + + public static List getSeedNodeAddressesFromPropertyFile(String fileName) { + List list = new ArrayList<>(); + readSeedNodePropertyFile(fileName).ifPresent(seedNodeFile -> { + seedNodeFile.lines().forEach(line -> { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) + list.add(new NodeAddress(matcher.group(1))); + + // Maybe better include in regex... + if (line.startsWith("localhost")) { + String[] strings = line.split(" \\(@"); + String node = strings[0]; + list.add(new NodeAddress(node)); + } + }); + }); + return list; + } + public Collection getSeedNodeAddresses() { if (cache.isEmpty()) reload(); diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index afa177c6733..d30a671bc8f 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -34,6 +34,8 @@ import bisq.core.dao.node.messages.GetBlocksResponse; import bisq.core.dao.node.messages.NewBlockBroadcastMessage; import bisq.core.filter.Filter; +import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; +import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; import bisq.core.offer.OfferPayload; import bisq.core.offer.messages.OfferAvailabilityRequest; import bisq.core.offer.messages.OfferAvailabilityResponse; @@ -64,8 +66,6 @@ import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.CloseConnectionMessage; import bisq.network.p2p.PrefixedSealedAndSignedMessage; -import bisq.network.p2p.inventory.messages.GetInventoryRequest; -import bisq.network.p2p.inventory.messages.GetInventoryResponse; import bisq.network.p2p.peers.getdata.messages.GetDataResponse; import bisq.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; diff --git a/core/src/main/resources/btc_regtest.seednodes b/core/src/main/resources/btc_regtest.seednodes index 148e6ecb9e0..44cd4ec296d 100644 --- a/core/src/main/resources/btc_regtest.seednodes +++ b/core/src/main/resources/btc_regtest.seednodes @@ -1,17 +1,3 @@ -# By default developers use either port 2002 or 3002 or both as local seed nodes. If they want to use regtest -# with Tor they have to add a program argument to pass the custom onion address of the local Tor seed node. -# E.g. --seedNodes=YOUR_ONION.onion:2002 - -# To create your local onion addresses follow those steps: -# 1. Run a seed node with prog args: --bitcoinNetwork=regtest --nodePort=2002 --appName=bisq_seed_node_localhost_YOUR_ONION -# 2. Find your local onion address in bisq_seed_node_localhost_YOUR_ONION/regtest/tor/hiddenservice/hostname -# 3. Shut down the seed node -# 4. Rename YOUR_ONION at the directory with your local onion address as well as the appName program argument to reflect -# the real onion address. -# 5. Start the seed node again -# 6. Start the Bisq app which wants to connect to that seed node with program argument `--seedNodes=YOUR_ONION.onion:2002` - - -# nodeaddress.onion:port [(@owner)] -localhost:2002 -localhost:3002 +# nodeaddress.onion:port [(@owner,@backup)] +localhost:2002 (@devtest1) +localhost:3002 (@devtest2) diff --git a/inventory/src/main/java/bisq/inventory/InventoryMonitor.java b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java new file mode 100644 index 00000000000..55b4565ceac --- /dev/null +++ b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java @@ -0,0 +1,216 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.inventory; + + +import bisq.core.network.p2p.inventory.GetInventoryRequestManager; +import bisq.core.network.p2p.inventory.model.Average; +import bisq.core.network.p2p.inventory.model.InventoryItem; +import bisq.core.network.p2p.inventory.model.RequestInfo; +import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; +import bisq.core.proto.network.CoreNetworkProtoResolver; + +import bisq.network.p2p.NetworkNodeProvider; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.SetupListener; + +import bisq.common.UserThread; +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.file.JsonFileManager; +import bisq.common.util.Utilities; + +import java.time.Clock; + +import java.io.File; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + +@Slf4j +public class InventoryMonitor implements SetupListener { + private final Map jsonFileManagerByNodeAddress = new HashMap<>(); + private final Map> requestInfoListByNode = new HashMap<>(); + private final File appDir; + private final boolean useLocalhostForP2P; + private final int intervalSec; + private final NetworkNode networkNode; + private final GetInventoryRequestManager getInventoryRequestManager; + + private ArrayList seedNodes; + private InventoryWebServer inventoryWebServer; + private int requestCounter = 0; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InventoryMonitor(File appDir, + boolean useLocalhostForP2P, + BaseCurrencyNetwork network, + int intervalSec, + int port) { + this.appDir = appDir; + this.useLocalhostForP2P = useLocalhostForP2P; + this.intervalSec = intervalSec; + + networkNode = getNetworkNode(appDir); + getInventoryRequestManager = new GetInventoryRequestManager(networkNode); + + // We maintain our own list as we want to monitor also old v2 nodes which are not part of the normal seed + // node list anymore. + String networkName = network.name().toLowerCase(); + String fileName = network.isMainnet() ? "inv_" + networkName : networkName; + DefaultSeedNodeRepository.readSeedNodePropertyFile(fileName) + .ifPresent(bufferedReader -> { + seedNodes = new ArrayList<>(DefaultSeedNodeRepository.getSeedNodeAddressesFromPropertyFile(fileName)); + addJsonFileManagers(seedNodes); + inventoryWebServer = new InventoryWebServer(port, seedNodes, bufferedReader); + networkNode.start(this); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown(Runnable shutDownCompleteHandler) { + networkNode.shutDown(shutDownCompleteHandler); + jsonFileManagerByNodeAddress.values().forEach(JsonFileManager::shutDown); + inventoryWebServer.shutDown(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // SetupListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onTorNodeReady() { + UserThread.runPeriodically(this::requestFromAllSeeds, intervalSec); + requestFromAllSeeds(); + } + + @Override + public void onHiddenServicePublished() { + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onRequestCustomBridges() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestFromAllSeeds() { + requestCounter++; + seedNodes.forEach(nodeAddress -> { + RequestInfo requestInfo = new RequestInfo(System.currentTimeMillis()); + new Thread(() -> { + Thread.currentThread().setName("request @ " + getShortAddress(nodeAddress, useLocalhostForP2P)); + getInventoryRequestManager.request(nodeAddress, + result -> processResponse(nodeAddress, requestInfo, result, null), + errorMessage -> processResponse(nodeAddress, requestInfo, null, errorMessage)); + }).start(); + }); + } + + private void processResponse(NodeAddress nodeAddress, + RequestInfo requestInfo, + @Nullable Map result, + @Nullable String errorMessage) { + if (errorMessage != null) { + log.warn("Error at connection to peer {}: {}", nodeAddress, errorMessage); + requestInfo.setErrorMessage(errorMessage); + } + + if (result != null) { + log.info("nodeAddress={}, result={}", nodeAddress, result.toString()); + requestInfo.setInventory(result); + long responseTime = System.currentTimeMillis(); + requestInfo.setResponseTime(responseTime); + } + + requestInfoListByNode.putIfAbsent(nodeAddress, new ArrayList<>()); + List requestInfoList = requestInfoListByNode.get(nodeAddress); + requestInfoList.add(requestInfo); + + // We create average of all nodes latest results. It might be that the nodes last result is + // from a previous request as the response has not arrived yet. + Set requestInfoSet = requestInfoListByNode.values().stream() + .filter(list -> !list.isEmpty()) + .map(list -> list.get(list.size() - 1)) + .collect(Collectors.toSet()); + Map averageValues = Average.of(requestInfoSet); + + inventoryWebServer.onNewRequestInfo(requestInfoListByNode, averageValues, requestCounter); + + String json = Utilities.objectToJson(requestInfo); + jsonFileManagerByNodeAddress.get(nodeAddress).writeToDisc(json, String.valueOf(requestInfo.getRequestStartTime())); + } + + private void addJsonFileManagers(List seedNodes) { + File jsonDir = new File(appDir, "json"); + if (!jsonDir.exists() && !jsonDir.mkdir()) { + log.warn("make jsonDir failed"); + } + seedNodes.forEach(nodeAddress -> { + JsonFileManager jsonFileManager = new JsonFileManager(new File(jsonDir, getShortAddress(nodeAddress, useLocalhostForP2P))); + jsonFileManagerByNodeAddress.put(nodeAddress, jsonFileManager); + }); + } + + private NetworkNode getNetworkNode(File appDir) { + File torDir = new File(appDir, "tor"); + CoreNetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone()); + return new NetworkNodeProvider(networkProtoResolver, + ArrayList::new, + useLocalhostForP2P, + 9999, + torDir, + null, + "", + -1, + "", + null, + false, + false).get(); + } + + private String getShortAddress(NodeAddress nodeAddress, boolean useLocalhostForP2P) { + return useLocalhostForP2P ? + nodeAddress.getFullAddress().replace(":", "_") : + nodeAddress.getFullAddress().substring(0, 10); + } +} diff --git a/inventory/src/main/java/bisq/inventory/InventoryMonitorMain.java b/inventory/src/main/java/bisq/inventory/InventoryMonitorMain.java new file mode 100644 index 00000000000..f6a58c8f77b --- /dev/null +++ b/inventory/src/main/java/bisq/inventory/InventoryMonitorMain.java @@ -0,0 +1,127 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.inventory; + + +import bisq.core.locale.Res; + +import bisq.common.UserThread; +import bisq.common.app.AsciiLogo; +import bisq.common.app.Log; +import bisq.common.app.Version; +import bisq.common.config.BaseCurrencyNetwork; +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.nio.file.Paths; + +import java.io.File; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import ch.qos.logback.classic.Level; + +import lombok.extern.slf4j.Slf4j; + + + +import sun.misc.Signal; + +@Slf4j +public class InventoryMonitorMain { + + private static InventoryMonitor inventoryMonitor; + private static boolean stopped; + + // prog args for regtest: 10 1 BTC_REGTEST + public static void main(String[] args) { + // Default values + int intervalSec = 300; + boolean useLocalhostForP2P = false; + BaseCurrencyNetwork network = BaseCurrencyNetwork.BTC_MAINNET; + int port = 80; + + if (args.length > 0) { + intervalSec = Integer.parseInt(args[0]); + } + if (args.length > 1) { + useLocalhostForP2P = args[1].equals("1"); + } + if (args.length > 2) { + network = BaseCurrencyNetwork.valueOf(args[2]); + } + if (args.length > 3) { + port = Integer.parseInt(args[3]); + } + + String appName = "bisq-InventoryMonitor-" + network + "-" + intervalSec; + File appDir = new File(Utilities.getUserDataDir(), appName); + if (!appDir.exists() && !appDir.mkdir()) { + log.warn("make appDir failed"); + } + inventoryMonitor = new InventoryMonitor(appDir, useLocalhostForP2P, network, intervalSec, port); + + setup(network, appDir); + } + + private static void setup(BaseCurrencyNetwork network, File appDir) { + AsciiLogo.showAsciiLogo(); + String logPath = Paths.get(appDir.getPath(), "bisq").toString(); + Log.setup(logPath); + Log.setLevel(Level.INFO); + Version.setBaseCryptoNetworkId(network.ordinal()); + + Res.setup(); // Used for some formatting in the webserver + + // We do not set any capabilities as we don't want to receive any network data beside our response. + // We also do not use capabilities for the request/response messages as we only connect to seeds nodes and + + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(inventoryMonitor.getClass().getSimpleName()) + .setDaemon(true) + .build(); + UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); + + Signal.handle(new Signal("INT"), signal -> { + shutDown(); + }); + + Signal.handle(new Signal("TERM"), signal -> { + shutDown(); + }); + keepRunning(); + } + + private static void shutDown() { + stopped = true; + inventoryMonitor.shutDown(() -> { + System.exit(0); + }); + } + + private static void keepRunning() { + while (!stopped) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException ignore) { + } + } + } +} diff --git a/inventory/src/main/java/bisq/inventory/InventoryWebServer.java b/inventory/src/main/java/bisq/inventory/InventoryWebServer.java new file mode 100644 index 00000000000..fec0a020de2 --- /dev/null +++ b/inventory/src/main/java/bisq/inventory/InventoryWebServer.java @@ -0,0 +1,397 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.inventory; + +import bisq.core.network.p2p.inventory.model.DeviationSeverity; +import bisq.core.network.p2p.inventory.model.InventoryItem; +import bisq.core.network.p2p.inventory.model.RequestInfo; +import bisq.core.util.FormattingUtils; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.util.MathUtils; +import bisq.common.util.Utilities; + +import java.io.BufferedReader; + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.Nullable; + + + +import spark.Spark; + +@Slf4j +public class InventoryWebServer { + private final static String CLOSE_TAG = "
"; + + private final List seedNodes; + private final Map operatorByNodeAddress = new HashMap<>(); + + private String html; + private int requestCounter; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InventoryWebServer(int port, + List seedNodes, + BufferedReader seedNodeFile) { + this.seedNodes = seedNodes; + setupOperatorMap(seedNodeFile); + + Spark.port(port); + Spark.get("/", (req, res) -> { + log.info("Incoming request from: {}", req.userAgent()); + return html == null ? "Starting up..." : html; + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onNewRequestInfo(Map> requestInfoListByNode, + Map averageValues, + int requestCounter) { + this.requestCounter = requestCounter; + html = generateHtml(requestInfoListByNode, averageValues); + } + + public void shutDown() { + Spark.stop(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // HTML + /////////////////////////////////////////////////////////////////////////////////////////// + + private String generateHtml(Map> map, + Map averageValues) { + StringBuilder html = new StringBuilder(); + html.append("" + + "" + + "

") + .append("Current time: ").append(new Date().toString()).append("
") + .append("Request cycle: ").append(requestCounter).append("
") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("").append(""); + + seedNodes.forEach(seedNode -> { + html.append(""); + if (map.containsKey(seedNode) && !map.get(seedNode).isEmpty()) { + List list = map.get(seedNode); + int numResponses = list.size(); + RequestInfo requestInfo = list.get(numResponses - 1); + html.append("") + .append("") + .append("") + .append("") + .append(""); + } else { + html.append("") + .append("") + .append("") + .append("") + .append(""); + } + html.append(""); + }); + + html.append("
Seed node infoRequest infoData inventoryDAO dataNetwork info
").append(getSeedNodeInfo(seedNode, requestInfo)).append("").append(getRequestInfo(requestInfo, numResponses)).append("").append(getDataInfo(requestInfo, averageValues, map)).append("").append(getDaoInfo(requestInfo, averageValues, map)).append("").append(getNetworkInfo(requestInfo, averageValues, map)).append("").append(getSeedNodeInfo(seedNode, null)).append("").append("n/a").append("").append("n/a").append("").append("n/a").append("").append("n/a").append("
"); + return html.toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Sub sections + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getSeedNodeInfo(NodeAddress nodeAddress, + @Nullable RequestInfo requestInfo) { + StringBuilder sb = new StringBuilder(); + + String operator = operatorByNodeAddress.get(nodeAddress.getFullAddress()); + sb.append("Operator: ").append(operator).append("
"); + + String address = nodeAddress.getFullAddress(); + sb.append("Node address: ").append(address).append("
"); + + if (requestInfo != null) { + sb.append("Version: ").append(requestInfo.getDisplayValue(InventoryItem.version)).append("
"); + String memory = requestInfo.getValue(InventoryItem.usedMemory); + String memoryString = memory != null ? Utilities.readableFileSize(Long.parseLong(memory)) : "n/a"; + sb.append("Memory used: ") + .append(memoryString) + .append("
"); + + String jvmStartTimeString = requestInfo.getValue(InventoryItem.jvmStartTime); + long jvmStartTime = jvmStartTimeString != null ? Long.parseLong(jvmStartTimeString) : 0; + sb.append("Node started at: ") + .append(new Date(jvmStartTime).toString()) + .append("
"); + + String duration = jvmStartTime > 0 ? + FormattingUtils.formatDurationAsWords(System.currentTimeMillis() - jvmStartTime, + true, true) : + "n/a"; + sb.append("Run duration: ").append(duration).append("
"); + } + + return sb.toString(); + } + + private String getRequestInfo(RequestInfo requestInfo, int numResponses) { + StringBuilder sb = new StringBuilder(); + + DeviationSeverity deviationSeverity = numResponses == requestCounter ? + DeviationSeverity.OK : + requestCounter - numResponses > 4 ? + DeviationSeverity.ALERT : + DeviationSeverity.WARN; + sb.append("Number of responses: ").append(getColorTagByDeviationSeverity(deviationSeverity)) + .append(numResponses).append(CLOSE_TAG); + + DeviationSeverity rrtDeviationSeverity = DeviationSeverity.OK; + String rrtString = "n/a"; + if (requestInfo.getResponseTime() > 0) { + long rrt = requestInfo.getResponseTime() - requestInfo.getRequestStartTime(); + if (rrt > 20_000) { + rrtDeviationSeverity = DeviationSeverity.ALERT; + } else if (rrt > 10_000) { + rrtDeviationSeverity = DeviationSeverity.WARN; + } + rrtString = MathUtils.roundDouble(rrt / 1000d, 3) + " sec"; + + } + sb.append("Round trip time: ").append(getColorTagByDeviationSeverity(rrtDeviationSeverity)) + .append(rrtString).append(CLOSE_TAG); + + Date requestStartTime = new Date(requestInfo.getRequestStartTime()); + sb.append("Requested at: ").append(requestStartTime).append("
"); + + String responseTime = requestInfo.getResponseTime() > 0 ? + new Date(requestInfo.getResponseTime()).toString() : + "n/a"; + sb.append("Response received at: ").append(responseTime).append("
"); + + String errorMessage = requestInfo.getErrorMessage(); + if (errorMessage != null && !errorMessage.isEmpty()) { + sb.append("Error message: ").append(getColorTagByDeviationSeverity(DeviationSeverity.ALERT)) + .append(errorMessage).append(CLOSE_TAG); + } + return sb.toString(); + } + + private String getDataInfo(RequestInfo requestInfo, + Map averageValues, + Map> map) { + StringBuilder sb = new StringBuilder(); + + sb.append(getLine(InventoryItem.OfferPayload, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.MailboxStoragePayload, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.TradeStatistics3, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.AccountAgeWitness, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.SignedWitness, requestInfo, averageValues, map.values())); + + sb.append(getLine(InventoryItem.Alert, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.Filter, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.Mediator, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.RefundAgent, requestInfo, averageValues, map.values())); + + return sb.toString(); + } + + private String getDaoInfo(RequestInfo requestInfo, + Map averageValues, + Map> map) { + StringBuilder sb = new StringBuilder(); + + sb.append(getLine("Number of BSQ blocks: ", InventoryItem.numBsqBlocks, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.TempProposalPayload, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.ProposalPayload, requestInfo, averageValues, map.values())); + sb.append(getLine(InventoryItem.BlindVotePayload, requestInfo, averageValues, map.values())); + sb.append(getLine("DAO state block height: ", InventoryItem.daoStateChainHeight, requestInfo, averageValues, map.values())); + + String daoStateChainHeight = null; + if (requestInfo.getInventory() != null && requestInfo.getInventory().containsKey(InventoryItem.daoStateChainHeight)) { + daoStateChainHeight = requestInfo.getInventory().get(InventoryItem.daoStateChainHeight); + } + + sb.append(getLine("DAO state hash: ", InventoryItem.daoStateHash, requestInfo, averageValues, map.values(), daoStateChainHeight)); + + // The hash for proposal changes only at first block of blind vote phase but as we do not want to initialize the + // dao domain we cannot check that. But we also don't need that as we can just compare that all hashes at all + // blocks from all seeds are the same. Same for blindVoteHash. + sb.append(getLine("Proposal state hash: ", InventoryItem.proposalHash, requestInfo, averageValues, map.values(), daoStateChainHeight)); + sb.append(getLine("Blind vote state hash: ", InventoryItem.blindVoteHash, requestInfo, averageValues, map.values(), daoStateChainHeight)); + + return sb.toString(); + } + + private String getNetworkInfo(RequestInfo requestInfo, + Map averageValues, + Map> map) { + StringBuilder sb = new StringBuilder(); + + sb.append(getLine("Max. connections: ", InventoryItem.maxConnections, requestInfo, averageValues, map.values())); + sb.append(getLine("Number of connections: ", InventoryItem.numConnections, requestInfo, averageValues, map.values())); + sb.append(getLine("Peak number of connections: ", InventoryItem.peakNumConnections, requestInfo, averageValues, map.values())); + sb.append(getLine("Number of 'All connections lost' events: ", InventoryItem.numAllConnectionsLostEvents, requestInfo, averageValues, map.values())); + + sb.append(getLine("Sent messages/sec: ", InventoryItem.sentMessagesPerSec, requestInfo, + averageValues, map.values(), null, this::getRounded)); + sb.append(getLine("Received messages/sec: ", InventoryItem.receivedMessagesPerSec, requestInfo, + averageValues, map.values(), null, this::getRounded)); + sb.append(getLine("Sent kB/sec: ", InventoryItem.sentBytesPerSec, requestInfo, + averageValues, map.values(), null, this::getRounded)); + sb.append(getLine("Received kB/sec: ", InventoryItem.receivedBytesPerSec, requestInfo, + averageValues, map.values(), null, this::getRounded)); + sb.append(getLine("Sent data: ", InventoryItem.sentBytes, requestInfo, + averageValues, map.values(), null, value -> Utilities.readableFileSize(Long.parseLong(value)))); + sb.append(getLine("Received data: ", InventoryItem.receivedBytes, requestInfo, + averageValues, map.values(), null, value -> Utilities.readableFileSize(Long.parseLong(value)))); + return sb.toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getLine(InventoryItem inventoryItem, + RequestInfo requestInfo, + Map averageValues, + Collection> collection) { + return getLine(getTitle(inventoryItem), + inventoryItem, + requestInfo, + averageValues, + collection); + } + + private String getLine(String title, + InventoryItem inventoryItem, + RequestInfo requestInfo, + Map averageValues, + Collection> collection) { + return getLine(title, + inventoryItem, + requestInfo, + averageValues, + collection, + null, + null); + } + + private String getLine(String title, + InventoryItem inventoryItem, + RequestInfo requestInfo, + Map averageValues, + Collection> collection, + @Nullable String daoStateChainHeight) { + return getLine(title, + inventoryItem, + requestInfo, + averageValues, + collection, + daoStateChainHeight, + null); + } + + private String getLine(String title, + InventoryItem inventoryItem, + RequestInfo requestInfo, + Map averageValues, + Collection> collection, + @Nullable String daoStateChainHeight, + @Nullable Function formatter) { + String displayValue = requestInfo.getDisplayValue(inventoryItem); + String value = requestInfo.getValue(inventoryItem); + if (formatter != null && value != null) { + displayValue = formatter.apply(value); + } + Double deviation = inventoryItem.getDeviation(averageValues, value); + DeviationSeverity deviationSeverity = inventoryItem.getDeviationSeverity(deviation, collection, value, daoStateChainHeight); + return title + + getColorTagByDeviationSeverity(deviationSeverity) + + displayValue + + getDeviationAsPercentString(deviation) + + CLOSE_TAG; + } + + private String getDeviationAsPercentString(@Nullable Double deviation) { + if (deviation == null) { + return ""; + } + + return " (" + MathUtils.roundDouble(100 * deviation, 2) + " %)"; + } + + private String getColorTagByDeviationSeverity(@Nullable DeviationSeverity deviationSeverity) { + if (deviationSeverity == null) { + return ""; + } + + switch (deviationSeverity) { + case WARN: + return ""; + case ALERT: + return ""; + case OK: + default: + return ""; + } + } + + private String getTitle(InventoryItem inventoryItem) { + return "Number of " + inventoryItem.getKey() + ": "; + } + + private String getRounded(String value) { + return String.valueOf(MathUtils.roundDouble(Double.parseDouble(value), 2)); + } + + private void setupOperatorMap(BufferedReader seedNodeFile) { + seedNodeFile.lines().forEach(line -> { + if (!line.startsWith("#")) { + String[] strings = line.split(" \\(@"); + String node = strings.length > 0 ? strings[0] : "n/a"; + String operator = strings.length > 1 ? strings[1].replace(")", "") : "n/a"; + operatorByNodeAddress.put(node, operator); + } + }); + } +} diff --git a/inventory/src/main/resources/inv_btc_mainnet.seednodes b/inventory/src/main/resources/inv_btc_mainnet.seednodes new file mode 100644 index 00000000000..8177bc54d43 --- /dev/null +++ b/inventory/src/main/resources/inv_btc_mainnet.seednodes @@ -0,0 +1,19 @@ +# nodeaddress.onion:port [(@owner,@backup)] +wizseedscybbttk4bmb2lzvbuk2jtect37lcpva4l3twktmkzemwbead.onion:8000 (@wiz) +wizseed3d376esppbmbjxk2fhk2jg5fpucddrzj2kxtbxbx4vrnwclad.onion:8000 (@wiz) +wizseed7ab2gi3x267xahrp2pkndyrovczezzb46jk6quvguciuyqrid.onion:8000 (@wiz) +sn3emzy56u3mxzsr4geysc52feoq5qt7ja56km6gygwnszkshunn2sid.onion:8000 (@emzy) +sn4emzywye3dhjouv7jig677qepg7fnusjidw74fbwneieruhmi7fuyd.onion:8000 (@emzy) +sn5emzyvxuildv34n6jewfp2zeota4aq63fsl5yyilnvksezr3htveqd.onion:8000 (@emzy) +sn2bisqad7ncazupgbd3dcedqh5ptirgwofw63djwpdtftwhddo75oid.onion:8000 (@miker) +sn3bsq3evqkpshdmc3sbdxafkhfnk7ctop44jsxbxyys5ridsaw5abyd.onion:8000 (@miker) +sn4bsqpc7eb2ntvpsycxbzqt6fre72l4krp2fl5svphfh2eusrqtq3qd.onion:8000 (@miker) +devinv3rhon24gqf5v6ondoqgyrbzyqihzyouzv7ptltsewhfmox2zqd.onion:8000 (@devinbileck) +devinsn2teu33efff62bnvwbxmfgbfjlgqsu3ad4b4fudx3a725eqnyd.onion:8000 (@devinbileck) +devinsn3xuzxhj6pmammrxpydhwwmwp75qkksedo5dn2tlmu7jggo7id.onion:8000 (@devinbileck) +723ljisnynbtdohi.onion:8000 (@emzy) +s67qglwhkgkyvr74.onion:8000 (@emzy) +5quyxpxheyvzmb2d.onion:8000 (@miker) +rm7b56wbrcczpjvl.onion:8000 (@miker) +3f3cu2yw7u457ztq.onion:8000 (@devinbileck) +fl3mmribyxgrv63c.onion:8000 (@devinbileck) diff --git a/inventory/src/main/resources/logback.xml b/inventory/src/main/resources/logback.xml new file mode 100644 index 00000000000..6b05588a4fe --- /dev/null +++ b/inventory/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + + + diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index 509419bf24d..927563476b5 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -19,8 +19,6 @@ import bisq.network.Socks5ProxyProvider; import bisq.network.crypto.EncryptionService; -import bisq.network.p2p.inventory.GetInventoryRequestHandler; -import bisq.network.p2p.inventory.GetInventoryRequestManager; import bisq.network.p2p.messaging.DecryptedMailboxListener; import bisq.network.p2p.network.CloseConnectionReason; import bisq.network.p2p.network.Connection; @@ -113,8 +111,6 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis private final SeedNodeRepository seedNodeRepository; private final EncryptionService encryptionService; private final KeyRing keyRing; - private final GetInventoryRequestHandler getInventoryRequestHandler; - private final GetInventoryRequestManager getInventoryRequestManager; private final NetworkNode networkNode; private final PeerManager peerManager; @@ -161,9 +157,7 @@ public P2PService(NetworkNode networkNode, SeedNodeRepository seedNodeRepository, Socks5ProxyProvider socks5ProxyProvider, EncryptionService encryptionService, - KeyRing keyRing, - GetInventoryRequestHandler getInventoryRequestHandler, - GetInventoryRequestManager getInventoryRequestManager) { + KeyRing keyRing) { this.networkNode = networkNode; this.peerManager = peerManager; this.p2PDataStorage = p2PDataStorage; @@ -175,8 +169,6 @@ public P2PService(NetworkNode networkNode, this.socks5ProxyProvider = socks5ProxyProvider; this.encryptionService = encryptionService; this.keyRing = keyRing; - this.getInventoryRequestHandler = getInventoryRequestHandler; - this.getInventoryRequestManager = getInventoryRequestManager; this.networkNode.addConnectionListener(this); this.networkNode.addMessageListener(this); @@ -267,9 +259,6 @@ private void doShutDown() { } else { shutDownResultHandlers.forEach(Runnable::run); } - - getInventoryRequestHandler.shutDown(); - getInventoryRequestManager.shutDown(); } diff --git a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestHandler.java b/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestHandler.java deleted file mode 100644 index 8695e71b37e..00000000000 --- a/p2p/src/main/java/bisq/network/p2p/inventory/GetInventoryRequestHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.network.p2p.inventory; - -import bisq.network.p2p.inventory.messages.GetInventoryRequest; -import bisq.network.p2p.inventory.messages.GetInventoryResponse; -import bisq.network.p2p.network.Connection; -import bisq.network.p2p.network.MessageListener; -import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.storage.P2PDataStorage; -import bisq.network.p2p.storage.payload.ProtectedStorageEntry; - -import bisq.common.proto.network.NetworkEnvelope; - -import javax.inject.Inject; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class GetInventoryRequestHandler implements MessageListener { - private final NetworkNode networkNode; - private final P2PDataStorage p2PDataStorage; - - @Inject - public GetInventoryRequestHandler(NetworkNode networkNode, P2PDataStorage p2PDataStorage) { - this.networkNode = networkNode; - this.p2PDataStorage = p2PDataStorage; - networkNode.addMessageListener(this); - } - - @Override - public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { - if (networkEnvelope instanceof GetInventoryRequest) { - GetInventoryRequest getInventoryRequest = (GetInventoryRequest) networkEnvelope; - - Map numPayloadsByClassName = new HashMap<>(); - p2PDataStorage.getMapForDataResponse(getInventoryRequest.getVersion()).values().stream() - .map(e -> e.getClass().getSimpleName()) - .forEach(className -> { - numPayloadsByClassName.putIfAbsent(className, 0); - int prev = numPayloadsByClassName.get(className); - numPayloadsByClassName.put(className, prev + 1); - }); - p2PDataStorage.getMap().values().stream() - .map(ProtectedStorageEntry::getProtectedStoragePayload) - .filter(Objects::nonNull) - .map(e -> e.getClass().getSimpleName()) - .forEach(className -> { - numPayloadsByClassName.putIfAbsent(className, 0); - int prev = numPayloadsByClassName.get(className); - numPayloadsByClassName.put(className, prev + 1); - }); - - GetInventoryResponse getInventoryResponse = new GetInventoryResponse(numPayloadsByClassName); - networkNode.sendMessage(connection, getInventoryResponse); - } - } - - public void shutDown() { - networkNode.removeMessageListener(this); - } -} diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index c6d3277ee35..3a03fd0231f 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -115,6 +115,7 @@ public enum PeerType { /////////////////////////////////////////////////////////////////////////////////////////// @Inject + @Nullable private static Config config; // Leaving some constants package-private for tests to know limits. @@ -249,10 +250,10 @@ public void sendMessage(NetworkEnvelope networkEnvelope) { // Throttle outbound network_messages long now = System.currentTimeMillis(); long elapsed = now - lastSendTimeStamp; - if (elapsed < config.sendMsgThrottleTrigger) { + if (elapsed < getSendMsgThrottleTrigger()) { log.debug("We got 2 sendMessage requests in less than {} ms. We set the thread to sleep " + "for {} ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}, networkEnvelope={}", - config.sendMsgThrottleTrigger, config.sendMsgThrottleSleep, lastSendTimeStamp, now, elapsed, + getSendMsgThrottleTrigger(), getSendMsgThrottleSleep(), lastSendTimeStamp, now, elapsed, networkEnvelope.getClass().getSimpleName()); // check if BundleOfEnvelopes is supported @@ -265,7 +266,7 @@ public void sendMessage(NetworkEnvelope networkEnvelope) { queueOfBundles.add(new BundleOfEnvelopes()); // - and schedule it for sending - lastSendTimeStamp += config.sendMsgThrottleSleep; + lastSendTimeStamp += getSendMsgThrottleSleep(); bundleSender.schedule(() -> { if (!stopped) { @@ -297,7 +298,7 @@ public void sendMessage(NetworkEnvelope networkEnvelope) { return; } - Thread.sleep(config.sendMsgThrottleSleep); + Thread.sleep(getSendMsgThrottleSleep()); } lastSendTimeStamp = now; @@ -372,11 +373,27 @@ private boolean violatesThrottleLimit() { messageTimeStamps.add(now); // clean list - while (messageTimeStamps.size() > config.msgThrottlePer10Sec) + while (messageTimeStamps.size() > getMsgThrottlePer10Sec()) messageTimeStamps.remove(0); - return violatesThrottleLimit(now, 1, config.msgThrottlePerSec) || - violatesThrottleLimit(now, 10, config.msgThrottlePer10Sec); + return violatesThrottleLimit(now, 1, getMsgThrottlePerSec()) || + violatesThrottleLimit(now, 10, getMsgThrottlePer10Sec()); + } + + private int getMsgThrottlePerSec() { + return config != null ? config.msgThrottlePerSec : 200; + } + + private int getMsgThrottlePer10Sec() { + return config != null ? config.msgThrottlePer10Sec : 1000; + } + + private int getSendMsgThrottleSleep() { + return config != null ? config.sendMsgThrottleSleep : 50; + } + + private int getSendMsgThrottleTrigger() { + return config != null ? config.sendMsgThrottleTrigger : 20; } private boolean violatesThrottleLimit(long now, int seconds, int messageCountLimit) { diff --git a/p2p/src/main/java/bisq/network/p2p/network/ConnectionListener.java b/p2p/src/main/java/bisq/network/p2p/network/ConnectionListener.java index 4e37f9c1f70..1b1891a2316 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/ConnectionListener.java +++ b/p2p/src/main/java/bisq/network/p2p/network/ConnectionListener.java @@ -22,5 +22,6 @@ public interface ConnectionListener { void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection); + //TODO is never called, can be removed void onError(Throwable throwable); } diff --git a/p2p/src/main/java/bisq/network/p2p/network/Statistic.java b/p2p/src/main/java/bisq/network/p2p/network/Statistic.java index 4573b9fae33..a7544238d9a 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Statistic.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Statistic.java @@ -47,7 +47,9 @@ public class Statistic { private final static long startTime = System.currentTimeMillis(); private final static LongProperty totalSentBytes = new SimpleLongProperty(0); + private final static DoubleProperty totalSentBytesPerSec = new SimpleDoubleProperty(0); private final static LongProperty totalReceivedBytes = new SimpleLongProperty(0); + private final static DoubleProperty totalReceivedBytesPerSec = new SimpleDoubleProperty(0); private final static Map totalReceivedMessages = new ConcurrentHashMap<>(); private final static Map totalSentMessages = new ConcurrentHashMap<>(); private final static LongProperty numTotalSentMessages = new SimpleLongProperty(0); @@ -63,6 +65,9 @@ public class Statistic { long passed = (System.currentTimeMillis() - startTime) / 1000; numTotalSentMessagesPerSec.set(((double) numTotalSentMessages.get()) / passed); numTotalReceivedMessagesPerSec.set(((double) numTotalReceivedMessages.get()) / passed); + + totalSentBytesPerSec.set(((double) totalSentBytes.get()) / passed); + totalReceivedBytesPerSec.set(((double) totalReceivedBytes.get()) / passed); }, 1); // We log statistics every minute @@ -87,10 +92,18 @@ public static LongProperty totalSentBytesProperty() { return totalSentBytes; } + public static DoubleProperty totalSentBytesPerSecProperty() { + return totalSentBytesPerSec; + } + public static LongProperty totalReceivedBytesProperty() { return totalReceivedBytes; } + public static DoubleProperty totalReceivedBytesPerSecProperty() { + return totalReceivedBytesPerSec; + } + public static LongProperty numTotalSentMessagesProperty() { return numTotalSentMessages; } diff --git a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java index c7d0b9e028b..a1aae19babc 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java @@ -23,7 +23,6 @@ import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.proto.network.NetworkProtoResolver; -import bisq.common.util.Utilities; import org.berndpruenster.netlayer.tor.HiddenServiceSocket; import org.berndpruenster.netlayer.tor.Tor; @@ -82,13 +81,14 @@ public class TorNetworkNode extends NetworkNode { private boolean streamIsolation; private Socks5Proxy socksProxy; + private ListenableFuture torStartupFuture; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TorNetworkNode(int servicePort, NetworkProtoResolver networkProtoResolver, boolean useStreamIsolation, - TorMode torMode) { + TorMode torMode) { super(servicePort, networkProtoResolver); this.torMode = torMode; this.streamIsolation = useStreamIsolation; @@ -153,72 +153,60 @@ public void shutDown(@Nullable Runnable shutDownCompleteHandler) { // this one is committed as a thread to the executor BooleanProperty torNetworkNodeShutDown = torNetworkNodeShutDown(); BooleanProperty shutDownTimerTriggered = shutDownTimerTriggered(); - // Need to store allShutDown to not get garbage collected - allShutDown = EasyBind.combine(torNetworkNodeShutDown, networkNodeShutDown, shutDownTimerTriggered, (a, b, c) -> (a && b) || c); + allShutDown = EasyBind.combine(torNetworkNodeShutDown, networkNodeShutDown, shutDownTimerTriggered, + (a, b, c) -> (a && b) || c); allShutDown.subscribe((observable, oldValue, newValue) -> { if (newValue) { shutDownTimeoutTimer.stop(); long ts = System.currentTimeMillis(); - log.debug("Shutdown executorService"); try { MoreExecutors.shutdownAndAwaitTermination(executorService, 500, TimeUnit.MILLISECONDS); - log.debug("Shutdown executorService done after " + (System.currentTimeMillis() - ts) + " ms."); - log.debug("Shutdown completed"); + log.debug("Shutdown executorService done after {} ms.", System.currentTimeMillis() - ts); } catch (Throwable t) { - log.error("Shutdown executorService failed with exception: " + t.getMessage()); + log.error("Shutdown executorService failed with exception: {}", t.getMessage()); t.printStackTrace(); } finally { - try { - if (shutDownCompleteHandler != null) - shutDownCompleteHandler.run(); - } catch (Throwable ignore) { - } + if (shutDownCompleteHandler != null) + shutDownCompleteHandler.run(); } } }); } private BooleanProperty torNetworkNodeShutDown() { - final BooleanProperty done = new SimpleBooleanProperty(); - if (executorService != null) { - executorService.submit(() -> { - Utilities.setThreadName("torNetworkNodeShutDown"); - long ts = System.currentTimeMillis(); - log.debug("Shutdown torNetworkNode"); - try { - /** - * make sure we get tor. - * - there have been situations where tor isn't set yet, which would leave tor running - * - downside is that if tor is not already started, we start it here just to shut it down. However, - * that can only be the case if Bisq gets shutdown even before it reaches step 2/4 at startup. - * The risk seems worth it compared to the risk of not shutting down tor. - */ - tor = Tor.getDefault(); - if (tor != null) - tor.shutdown(); - log.debug("Shutdown torNetworkNode done after " + (System.currentTimeMillis() - ts) + " ms."); - } catch (Throwable e) { - log.error("Shutdown torNetworkNode failed with exception: " + e.getMessage()); - e.printStackTrace(); - } finally { - UserThread.execute(() -> done.set(true)); - } - }); - } else { - done.set(true); + BooleanProperty done = new SimpleBooleanProperty(); + try { + tor = Tor.getDefault(); + if (tor != null) { + log.info("Tor has been created already so we can shut it down."); + tor.shutdown(); + log.info("Tor shut down completed"); + } else { + log.info("Tor has not been created yet. We cancel the torStartupFuture."); + torStartupFuture.cancel(true); + log.info("torStartupFuture cancelled"); + } + } catch (Throwable e) { + log.error("Shutdown torNetworkNode failed with exception: {}", e.getMessage()); + e.printStackTrace(); + + } finally { + // We need to delay as otherwise our listener would not get called if shutdown completes in synchronous manner + UserThread.execute(() -> done.set(true)); } return done; } private BooleanProperty networkNodeShutDown() { - final BooleanProperty done = new SimpleBooleanProperty(); - super.shutDown(() -> done.set(true)); + BooleanProperty done = new SimpleBooleanProperty(); + // We need to delay as otherwise our listener would not get called if shutdown completes in synchronous manner + UserThread.execute(() -> super.shutDown(() -> done.set(true))); return done; } private BooleanProperty shutDownTimerTriggered() { - final BooleanProperty done = new SimpleBooleanProperty(); + BooleanProperty done = new SimpleBooleanProperty(); shutDownTimeoutTimer = UserThread.runAfter(() -> { log.error("A timeout occurred at shutDown"); done.set(true); @@ -255,7 +243,7 @@ private void restartTor(String errorMessage) { /////////////////////////////////////////////////////////////////////////////////////////// private void createTorAndHiddenService(int localPort, int servicePort) { - ListenableFuture future = executorService.submit(() -> { + torStartupFuture = executorService.submit(() -> { try { // get tor Tor.setDefault(torMode.getTor()); @@ -313,7 +301,7 @@ public void run() { return null; }); - Futures.addCallback(future, new FutureCallback() { + Futures.addCallback(torStartupFuture, new FutureCallback() { public void onSuccess(Void ignore) { } diff --git a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java index 20af0453194..6b3a56c71e4 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/PeerManager.java @@ -81,6 +81,7 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost // Age of what we consider connected peers still as live peers private static final long MAX_AGE_LIVE_PEERS = TimeUnit.MINUTES.toMillis(30); private static final boolean PRINT_REPORTED_PEERS_DETAILS = true; + private boolean shutDownRequested; /////////////////////////////////////////////////////////////////////////////////////////// @@ -126,8 +127,12 @@ public interface Listener { private int maxConnectionsPeer; private int maxConnectionsNonDirect; private int maxConnectionsAbsolute; + @Getter + private int peakNumConnections; @Setter private boolean allowDisconnectSeedNodes; + @Getter + private int numAllConnectionsLostEvents; /////////////////////////////////////////////////////////////////////////////////////////// @@ -172,6 +177,7 @@ public void onAwakeFromStandby(long missedMs) { } public void shutDown() { + shutDownRequested = true; networkNode.removeConnectionListener(this); clockWatcher.removeListener(clockWatcherListener); stopCheckMaxConnectionsTimer(); @@ -206,6 +212,9 @@ public void onConnection(Connection connection) { if (lostAllConnections) { lostAllConnections = false; stopped = false; + log.info("\n------------------------------------------------------------\n" + + "Established a new connection from/to {} after all connections lost.\n" + + "------------------------------------------------------------", connection.getPeersNodeAddressOptional()); listeners.forEach(Listener::onNewConnectionAfterAllConnectionsLost); } connection.getPeersNodeAddressOptional() @@ -218,13 +227,25 @@ public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection log.info("onDisconnect called: nodeAddress={}, closeConnectionReason={}", connection.getPeersNodeAddressOptional(), closeConnectionReason); handleConnectionFault(connection); + + boolean previousLostAllConnections = lostAllConnections; lostAllConnections = networkNode.getAllConnections().isEmpty(); + if (lostAllConnections) { stopped = true; - log.warn("\n------------------------------------------------------------\n" + - "All connections lost\n" + - "------------------------------------------------------------"); - listeners.forEach(Listener::onAllConnectionsLost); + + if (!shutDownRequested) { + if (!previousLostAllConnections) { + // If we enter to 'All connections lost' we count the event. + numAllConnectionsLostEvents++; + } + + log.warn("\n------------------------------------------------------------\n" + + "All connections lost\n" + + "------------------------------------------------------------"); + + listeners.forEach(Listener::onAllConnectionsLost); + } } maybeRemoveBannedPeer(closeConnectionReason, connection); } @@ -442,6 +463,10 @@ private void doHouseKeeping() { checkMaxConnectionsTimer = UserThread.runAfter(() -> { stopCheckMaxConnectionsTimer(); if (!stopped) { + Set allConnections = new HashSet<>(networkNode.getAllConnections()); + int size = allConnections.size(); + peakNumConnections = Math.max(peakNumConnections, size); + removeAnonymousPeers(); removeSuperfluousSeedNodes(); removeTooOldReportedPeers(); @@ -458,6 +483,7 @@ private void doHouseKeeping() { boolean checkMaxConnections() { Set allConnections = new HashSet<>(networkNode.getAllConnections()); int size = allConnections.size(); + peakNumConnections = Math.max(peakNumConnections, size); log.info("We have {} connections open. Our limit is {}", size, maxConnections); if (size <= maxConnections) { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 8323a58c4ac..199918b8f47 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -147,7 +147,7 @@ message GetInventoryRequest { } message GetInventoryResponse { - map num_payloads_map = 1; + map inventory = 1; } // offer diff --git a/seednode/src/main/java/bisq/seednode/SeedNode.java b/seednode/src/main/java/bisq/seednode/SeedNode.java index e372defbee8..ff2f2609d3d 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNode.java +++ b/seednode/src/main/java/bisq/seednode/SeedNode.java @@ -19,6 +19,7 @@ import bisq.core.app.misc.AppSetup; import bisq.core.app.misc.AppSetupWithP2PAndDAO; +import bisq.core.network.p2p.inventory.GetInventoryRequestHandler; import com.google.inject.Injector; @@ -30,6 +31,7 @@ public class SeedNode { @Setter private Injector injector; private AppSetup appSetup; + private GetInventoryRequestHandler getInventoryRequestHandler; public SeedNode() { } @@ -37,5 +39,11 @@ public SeedNode() { public void startApplication() { appSetup = injector.getInstance(AppSetupWithP2PAndDAO.class); appSetup.start(); + + getInventoryRequestHandler = injector.getInstance(GetInventoryRequestHandler.class); + } + + public void shutDown() { + getInventoryRequestHandler.shutDown(); } } diff --git a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java index c02811ce425..37c75da5885 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java @@ -27,6 +27,7 @@ import bisq.common.app.AppModule; import bisq.common.app.Capabilities; import bisq.common.app.Capability; +import bisq.common.handlers.ResultHandler; import lombok.extern.slf4j.Slf4j; @@ -39,7 +40,7 @@ public SeedNodeMain() { super("Bisq Seednode", "bisq-seednode", "bisq_seednode", VERSION); } - public static void main(String[] args) throws Exception { + public static void main(String[] args) { log.info("SeedNode.VERSION: " + VERSION); new SeedNodeMain().execute(args); } @@ -138,4 +139,10 @@ public void onRequestCustomBridges() { } }); } + + @Override + public void gracefulShutDown(ResultHandler resultHandler) { + seedNode.shutDown(); + super.gracefulShutDown(resultHandler); + } } diff --git a/settings.gradle b/settings.gradle index 45ca5993367..1827ef9c4fa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include 'pricenode' include 'relay' include 'seednode' include 'statsnode' +include 'inventory' include 'apitest' rootProject.name = 'bisq'