From 73cc3dcb92184618d11196331fd593c22d4e89b5 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 19 Jun 2021 13:03:46 +0100 Subject: [PATCH] Force a disconnect of each peer when the connection age reaches the maximum allowed time. Connection limits are defined in settings (denominated in seconds): "minPeerConnectionTime": 120, "maxPeerConnectionTime": 3600 Peers will disconnect after a randomly chosen amount of time between the min and the max. The default range is 2 minutes to 1 hour, as above. This encourages nodes to connect to a wider range of peers across the course of each day, rather than staying connected to an "island" of peers for an extended period of time. Hopefully this will reduce the amount of parallel chains that can form due to permanently connected clusters of peers. We may find that we need to reduce the defaults to get optimal results, however it is best to do this incrementally, with the option for reducing further via each node's settings. Being too aggressive here could cause some of the earlier problems (e.g. 20% missing blocks minted) to reappear. We can re-evaluate this in the next version. Note that if defaults are reduced significantly, we may need to add code to prevent this from happening mid-sync. With higher defaults, this is less of an issue. Thanks to @szisti for supplying some base code for this commit, and also to @CWDSYSTEMS for diagnosing the original problem. --- src/main/java/org/qortal/network/Network.java | 24 +++++++++++++++++++ src/main/java/org/qortal/network/Peer.java | 23 ++++++++++++++++++ .../java/org/qortal/settings/Settings.java | 9 +++++++ 3 files changed, 56 insertions(+) diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index 0ef7d5a0..dd7cb01f 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -80,6 +80,8 @@ public class Network { public static final int MAX_SIGNATURES_PER_REPLY = 500; public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500; + private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds + // Generate our node keys / ID private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom()); private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey(); @@ -89,6 +91,8 @@ public class Network { private final int minOutboundPeers; private final int maxPeers; + private long nextDisconnectionCheck = 0L; + private final List allKnownPeers = new ArrayList<>(); private final List connectedPeers = new ArrayList<>(); private final List selfPeers = new ArrayList<>(); @@ -576,6 +580,8 @@ public class Network { // Don't consider already connected peers (resolved address match) // XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS peers.removeIf(isResolvedAsConnectedPeer); + + this.checkLongestConnection(now); } // Any left? @@ -633,6 +639,24 @@ public class Network { return null; } + private void checkLongestConnection(Long now) { + if (now == null || now < nextDisconnectionCheck) { + return; + } + + // Find peers that have reached their maximum connection age, and disconnect them + List peersToDisconnect = this.connectedPeers.stream().filter(peer -> peer.hasReachedMaxConnectionAge()).collect(Collectors.toList()); + if (peersToDisconnect != null && peersToDisconnect.size() > 0) { + for (Peer peer : peersToDisconnect) { + LOGGER.debug("Forcing disconnect of peer {} because connection age ({} ms) has reached the maximum ({} ms)", peer, peer.getConnectionAge(), peer.getMaxConnectionAge()); + peer.disconnect("Connection age too old"); + } + } + + // Check again after a minimum fixed interval + nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL; + } + // Peer callbacks protected void wakeupChannelSelector() { diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index c84d1118..66ef3ae0 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -79,6 +79,7 @@ public class Peer { private Handshake handshakeStatus = Handshake.STARTED; private volatile boolean handshakeMessagePending = false; private long handshakeComplete = -1L; + private long maxConnectionAge = 0L; /** * Timestamp of when socket was accepted, or connected. @@ -192,10 +193,24 @@ public class Peer { this.handshakeStatus = handshakeStatus; if (handshakeStatus.equals(Handshake.COMPLETED)) { this.handshakeComplete = System.currentTimeMillis(); + this.generateRandomMaxConnectionAge(); } } } + private void generateRandomMaxConnectionAge() { + // Retrieve the min and max connection time from the settings, and calculate the range + final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime(); + final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime(); + final int peerConnectionTimeRange = maxPeerConnectionTime - minPeerConnectionTime; + + // Generate a random number between the min and the max + Random random = new Random(); + this.maxConnectionAge = (random.nextInt(peerConnectionTimeRange) + minPeerConnectionTime) * 1000L; + LOGGER.debug(String.format("Generated max connection age for peer %s. Min: %ds, max: %ds, range: %ds, random max: %dms", this, minPeerConnectionTime, maxPeerConnectionTime, peerConnectionTimeRange, this.maxConnectionAge)); + + } + protected void resetHandshakeMessagePending() { this.handshakeMessagePending = false; } @@ -777,4 +792,12 @@ public class Peer { } return handshakeComplete; } + + public long getMaxConnectionAge() { + return maxConnectionAge; + } + + public boolean hasReachedMaxConnectionAge() { + return this.getConnectionAge() > this.getMaxConnectionAge(); + } } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 164223c9..7f67195e 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -132,6 +132,11 @@ public class Settings { * If false, sync will be blocked both ways, and they will not appear in the peers list */ private boolean allowConnectionsWithOlderPeerVersions = true; + /** Minimum time (in seconds) that we should attempt to remain connected to a peer for */ + private int minPeerConnectionTime = 2 * 60; + /** Maximum time (in seconds) that we should attempt to remain connected to a peer for */ + private int maxPeerConnectionTime = 60 * 60; + // Which blockchains this node is running private String blockchainConfig = null; // use default from resources private BitcoinNet bitcoinNet = BitcoinNet.MAIN; @@ -423,6 +428,10 @@ public class Settings { public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; } + public int getMinPeerConnectionTime() { return this.minPeerConnectionTime; } + + public int getMaxPeerConnectionTime() { return this.maxPeerConnectionTime; } + public String getBlockchainConfig() { return this.blockchainConfig; }