diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index 39daccd6..ad36c6ee 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -91,12 +91,13 @@ public class Controller extends Thread { private static final String repositoryUrlTemplate = "jdbc:hsqldb:file:%s/blockchain;create=true;hsqldb.full_log_replay=true"; private static final long ARBITRARY_REQUEST_TIMEOUT = 5 * 1000; // ms private static final long REPOSITORY_BACKUP_PERIOD = 123 * 60 * 1000; // ms - private static final long NTP_CHECK_PERIOD = 5 * 60 * 1000; // ms + private static final long NTP_CHECK_PERIOD = 10 * 60 * 1000; // ms private static final long MAX_NTP_OFFSET = 500; // ms private static volatile boolean isStopping = false; private static BlockGenerator blockGenerator = null; private static volatile boolean requestSync = false; + private static volatile boolean requestSysTrayUpdate = false; private static Controller instance; private final String buildVersion; @@ -105,7 +106,7 @@ public class Controller extends Thread { private long repositoryBackupTimestamp = startTime + REPOSITORY_BACKUP_PERIOD; private long ntpCheckTimestamp = startTime; // ms /** Whether BlockGenerator is allowed to generate blocks. Mostly determined by system clock accuracy. */ - private boolean isGenerationAllowed = false; + private volatile boolean isGenerationAllowed = false; /** Signature of peer's latest block when we tried to sync but peer had inferior chain. */ private byte[] inferiorChainPeerBlockSignature = null; @@ -329,11 +330,8 @@ public class Controller extends Thread { // Potentially nag end-user about NTP if (System.currentTimeMillis() >= ntpCheckTimestamp) { ntpCheckTimestamp += NTP_CHECK_PERIOD; - Boolean isClockAccurate = ntpCheck(); - if (isClockAccurate != null) { - isGenerationAllowed = isClockAccurate; - updateSysTray(); - } + isGenerationAllowed = ntpCheck(); + requestSysTrayUpdate = true; } // Prune stuck/slow/old peers @@ -342,6 +340,12 @@ public class Controller extends Thread { } catch (DataException e) { LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage())); } + + // Maybe update SysTray + if (requestSysTrayUpdate) { + requestSysTrayUpdate = false; + updateSysTray(); + } } } catch (InterruptedException e) { // Fall-through to exit @@ -418,7 +422,7 @@ public class Controller extends Thread { break; case OK: - updateSysTray(); + requestSysTrayUpdate = true; // fall-through... case NOTHING_TO_DO: LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name())); @@ -435,14 +439,14 @@ public class Controller extends Thread { /** * Nag if we detect system clock is too far from internet time. * - * @return true if clock is accurate, false if inaccurate, null if we don't know. + * @return true if clock is accurate, false if inaccurate or we don't know. */ - private Boolean ntpCheck() { + private boolean ntpCheck() { // Fetch mean offset from internet time (ms). Long meanOffset = NTP.getOffset(); final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - Boolean isNtpActive = null; + boolean isNtpActive = false; if (isWindows) { // Detecting Windows Time service @@ -472,24 +476,22 @@ public class Controller extends Thread { } } - // If offset is good and ntp is active then we're good - if (meanOffset != null && Math.abs(meanOffset) < MAX_NTP_OFFSET && isNtpActive == true) - return true; + LOGGER.info(String.format("NTP mean offset %s, NTP service active: %s", meanOffset, isNtpActive)); - // Time to nag - String caption = Translator.INSTANCE.translate("SysTray", "NTP_NAG_CAPTION"); - String text = Translator.INSTANCE.translate("SysTray", isWindows ? "NTP_NAG_TEXT_WINDOWS" : "NTP_NAG_TEXT_UNIX"); - SysTray.getInstance().showMessage(caption, text, MessageType.WARNING); + final boolean isOffsetGood = meanOffset != null && Math.abs(meanOffset) < MAX_NTP_OFFSET; - if (meanOffset == null) - // We don't know if we're inaccurate - return null; + // If offset bad or NTP not active then nag + if (!isOffsetGood || !isNtpActive) { + String caption = Translator.INSTANCE.translate("SysTray", "NTP_NAG_CAPTION"); + String text = Translator.INSTANCE.translate("SysTray", isWindows ? "NTP_NAG_TEXT_WINDOWS" : "NTP_NAG_TEXT_UNIX"); + SysTray.getInstance().showMessage(caption, text, MessageType.WARNING); + } // Return whether we're accurate (disregarding whether NTP service is active) - return Math.abs(meanOffset) < MAX_NTP_OFFSET; + return isOffsetGood; } - public void updateSysTray() { + private void updateSysTray() { final int numberOfPeers = Network.getInstance().getUniqueHandshakedPeers().size(); final int height = getChainHeight(); @@ -565,17 +567,14 @@ public class Controller extends Thread { public void doNetworkBroadcast() { Network network = Network.getInstance(); - // Send our known peers - network.broadcast(peer -> network.buildPeersMessage(peer)); + // Send (if outbound) / Request peer lists + network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage()); // Send our current height BlockData latestBlockData = getChainTip(); network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData)); - // Request peers lists - network.broadcast(peer -> new GetPeersMessage()); - - // Request unconfirmed transaction signatures + // Send (if outbound) / Request unconfirmed transaction signatures network.broadcast(peer -> network.buildGetUnconfirmedTransactionsMessage(peer)); } @@ -594,39 +593,42 @@ public class Controller extends Thread { } public void onPeerHandshakeCompleted(Peer peer) { - if (peer.getVersion() < 2) { - // Legacy mode + // Only send if outbound + if (peer.isOutbound()) { + if (peer.getVersion() < 2) { + // Legacy mode - // Send our unconfirmed transactions - try (final Repository repository = RepositoryManager.getRepository()) { - List transactions = repository.getTransactionRepository().getUnconfirmedTransactions(); + // Send our unconfirmed transactions + try (final Repository repository = RepositoryManager.getRepository()) { + List transactions = repository.getTransactionRepository().getUnconfirmedTransactions(); - for (TransactionData transactionData : transactions) { - Message transactionMessage = new TransactionMessage(transactionData); - if (!peer.sendMessage(transactionMessage)) { - peer.disconnect("failed to send unconfirmed transaction"); - return; + for (TransactionData transactionData : transactions) { + Message transactionMessage = new TransactionMessage(transactionData); + if (!peer.sendMessage(transactionMessage)) { + peer.disconnect("failed to send unconfirmed transaction"); + return; + } } + } catch (DataException e) { + LOGGER.error("Repository issue while sending unconfirmed transactions", e); } - } catch (DataException e) { - LOGGER.error("Repository issue while sending unconfirmed transactions", e); - } - } else { - // V2 protocol + } else { + // V2 protocol - // Request peer's unconfirmed transactions - Message message = new GetUnconfirmedTransactionsMessage(); - if (!peer.sendMessage(message)) { - peer.disconnect("failed to send request for unconfirmed transactions"); - return; + // Request peer's unconfirmed transactions + Message message = new GetUnconfirmedTransactionsMessage(); + if (!peer.sendMessage(message)) { + peer.disconnect("failed to send request for unconfirmed transactions"); + return; + } } } - updateSysTray(); + requestSysTrayUpdate = true; } public void onPeerDisconnect(Peer peer) { - updateSysTray(); + requestSysTrayUpdate = true; } public void onNetworkMessage(Peer peer, Message message) { @@ -655,10 +657,17 @@ public class Controller extends Thread { case HEIGHT_V2: { HeightV2Message heightV2Message = (HeightV2Message) message; + // If peer is inbound and we've not updated their height + // then this is probably their initial HEIGHT_V2 message + // so they need a corresponding HEIGHT_V2 message from us + if (!peer.isOutbound() && peer.getLastHeight() == null) + peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip())); + // Update all peers with same ID List connectedPeers = Network.getInstance().getHandshakedPeers(); for (Peer connectedPeer : connectedPeers) { + // Skip connectedPeer if they have no ID or their ID doesn't match sender's ID if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId())) continue; diff --git a/src/main/java/org/qora/network/Network.java b/src/main/java/org/qora/network/Network.java index 70bb2c55..f5d9da25 100644 --- a/src/main/java/org/qora/network/Network.java +++ b/src/main/java/org/qora/network/Network.java @@ -63,7 +63,7 @@ public class Network extends Thread { private static final int LISTEN_BACKLOG = 10; /** How long before retrying after a connection failure, in milliseconds. */ - private static final int CONNECT_FAILURE_BACKOFF = 60 * 1000; // ms + private static final int CONNECT_FAILURE_BACKOFF = 5 * 60 * 1000; // ms /** How long between informational broadcasts to all connected peers, in milliseconds. */ private static final int BROADCAST_INTERVAL = 60 * 1000; // ms /** Maximum time since last successful connection for peer info to be propagated, in milliseconds. */ @@ -508,7 +508,7 @@ public class Network extends Thread { handshakePeers.removeIf(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED || peer.getConnectionTimestamp() == null || peer.getConnectionTimestamp() > now - HANDSHAKE_TIMEOUT); for (Peer peer : handshakePeers) - peer.disconnect("handshake timeout"); + peer.disconnect(String.format("handshake timeout at %s", peer.getHandshakeStatus().name())); // Prune 'old' peers from repository... try (final Repository repository = RepositoryManager.getRepository()) { @@ -848,24 +848,29 @@ public class Network extends Thread { // Start regular pings peer.startPings(); - // Send our height - Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip()); - if (!peer.sendMessage(heightMessage)) { - peer.disconnect("failed to send height/info"); - return; + // Only the outbound side needs to send anything (after we've received handshake-completing response). + // (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message). + + if (peer.isOutbound()) { + // Send our height + Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip()); + if (!peer.sendMessage(heightMessage)) { + peer.disconnect("failed to send height/info"); + return; + } + + // Send our peers list + Message peersMessage = this.buildPeersMessage(peer); + if (!peer.sendMessage(peersMessage)) + peer.disconnect("failed to send peers list"); + + // Request their peers list + Message getPeersMessage = new GetPeersMessage(); + if (!peer.sendMessage(getPeersMessage)) + peer.disconnect("failed to request peers list"); } - // Send our peers list - Message peersMessage = this.buildPeersMessage(peer); - if (!peer.sendMessage(peersMessage)) - peer.disconnect("failed to send peers list"); - - // Request their peers list - Message getPeersMessage = new GetPeersMessage(); - if (!peer.sendMessage(getPeersMessage)) - peer.disconnect("failed to request peers list"); - - // Ask Controller if they want to send anything + // Ask Controller if they want to do anything Controller.getInstance().onPeerHandshakeCompleted(peer); } diff --git a/src/main/java/org/qora/network/Peer.java b/src/main/java/org/qora/network/Peer.java index ee828ac4..068cdcb6 100644 --- a/src/main/java/org/qora/network/Peer.java +++ b/src/main/java/org/qora/network/Peer.java @@ -483,7 +483,8 @@ public class Peer { } public void disconnect(String reason) { - LOGGER.debug(String.format("Disconnecting peer %s: %s", this, reason)); + if (!isStopping) + LOGGER.debug(() -> String.format("Disconnecting peer %s: %s", this, reason)); this.shutdown(); @@ -491,7 +492,10 @@ public class Peer { } public void shutdown() { - LOGGER.debug(() -> String.format("Shutting down peer %s", this)); + if (!isStopping) + LOGGER.debug(() -> String.format("Shutting down peer %s", this)); + + isStopping = true; if (this.socketChannel.isOpen()) { try { diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 741416ff..14bea809 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -102,11 +102,11 @@ public class Settings { "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org", - "asia.pool.ntp.org", - "0.asia.pool.ntp.org", - "1.asia.pool.ntp.org", - "2.asia.pool.ntp.org", - "3.asia.pool.ntp.org" + "cn.pool.ntp.org", + "0.cn.pool.ntp.org", + "1.cn.pool.ntp.org", + "2.cn.pool.ntp.org", + "3.cn.pool.ntp.org" }; // Constructors diff --git a/src/main/java/org/qora/utils/NTP.java b/src/main/java/org/qora/utils/NTP.java index 414f2fc8..16529393 100644 --- a/src/main/java/org/qora/utils/NTP.java +++ b/src/main/java/org/qora/utils/NTP.java @@ -14,7 +14,7 @@ import org.qora.settings.Settings; public class NTP { private static final Logger LOGGER = LogManager.getLogger(NTP.class); - private static final double MAX_STDDEV = 25; // ms + private static final double MAX_STDDEV = 125; // ms /** * Returns aggregated internet time. @@ -71,7 +71,7 @@ public class NTP { } if (offsets.size() < ntpServers.length / 2) { - LOGGER.debug("Not enough replies"); + LOGGER.info(String.format("Not enough replies: %d, minimum is %d", offsets.size(), ntpServers.length / 2)); return null; } @@ -90,8 +90,10 @@ public class NTP { double stddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1))); // If stddev is excessive then we're not very sure so give up - if (stddev > MAX_STDDEV) + if (stddev > MAX_STDDEV) { + LOGGER.info(String.format("Excessive standard deviation %.1f, maximum is %.1f", stddev, MAX_STDDEV)); return null; + } return (long) mean; }