diff --git a/pom.xml b/pom.xml
index 7b7e866f..c4c786d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -335,13 +335,12 @@
commons-text
1.4
-
commons-net
commons-net
3.3
- -->
com.google.guava
guava
diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java
index 426d6deb..baacd1c3 100644
--- a/src/main/java/org/qora/controller/Controller.java
+++ b/src/main/java/org/qora/controller/Controller.java
@@ -70,6 +70,7 @@ import org.qora.transaction.Transaction.TransactionType;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.ui.UiService;
import org.qora.utils.Base58;
+import org.qora.utils.NTP;
import org.qora.utils.Triple;
public class Controller extends Thread {
@@ -91,6 +92,7 @@ public class Controller extends Thread {
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_NAG_PERIOD = 5 * 60 * 1000; // ms
+ private static final long MAX_NTP_OFFSET = 500; // ms
private static volatile boolean isStopping = false;
private static BlockGenerator blockGenerator = null;
@@ -420,46 +422,50 @@ public class Controller extends Thread {
}
}
- /** Nag Windows users that don't have many/any peers and not using Windows' auto time sync. */
+ /** Nag if we detect system clock is too far from internet time. */
private void ntpNag() {
- // Only for Windows users
- if (!System.getProperty("os.name").toLowerCase().contains("win"))
- return;
+ // Fetch mean offset from internet time (ms).
+ Long meanOffset = NTP.getOffset();
- // Suffering from lack of peers?
- final int numberOfPeers = Network.getInstance().getUniqueHandshakedPeers().size();
- if (numberOfPeers > 0)
- return;
+ final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
+ Boolean isNtpActive = null;
+ if (isWindows) {
+ // Detecting Windows Time service
- // Do we actually know any peers to connect to?
- try (final Repository repository = RepositoryManager.getRepository()) {
- final int numberOfKnownPeers = repository.getNetworkRepository().getAllPeers().size();
- if (numberOfKnownPeers == 0)
- return;
- } catch (DataException e) {
- // Not important
- return;
- }
-
- String[] detectCmd = new String[] { "net", "start" };
- try {
- Process process = new ProcessBuilder(Arrays.asList(detectCmd)).start();
- try (InputStream in = process.getInputStream(); Scanner scanner = new Scanner(in, "UTF8")) {
- scanner.useDelimiter("\\A");
- String output = scanner.hasNext() ? scanner.next() : "";
- boolean isRunning = output.contains("Windows Time");
- if (isRunning)
- return;
+ String[] detectCmd = new String[] { "net", "start" };
+ try {
+ Process process = new ProcessBuilder(Arrays.asList(detectCmd)).start();
+ try (InputStream in = process.getInputStream(); Scanner scanner = new Scanner(in, "UTF8")) {
+ scanner.useDelimiter("\\A");
+ String output = scanner.hasNext() ? scanner.next() : "";
+ isNtpActive = output.contains("Windows Time");
+ }
+ } catch (IOException e) {
+ // Not important
+ }
+ } else {
+ // Very basic unix-based attempt to check for ntpd
+ String[] detectCmd = new String[] { "ps", "-agx" };
+ try {
+ Process process = new ProcessBuilder(Arrays.asList(detectCmd)).start();
+ try (InputStream in = process.getInputStream(); Scanner scanner = new Scanner(in, "UTF8")) {
+ scanner.useDelimiter("\\A");
+ String output = scanner.hasNext() ? scanner.next() : "";
+ isNtpActive = output.contains("ntpd");
+ }
+ } catch (IOException e) {
+ // Not important
}
- } catch (IOException e) {
- // Not important
- return;
}
+ // If offset is good and ntp is active then we're good
+ if (Math.abs(meanOffset) < MAX_NTP_OFFSET && isNtpActive == true)
+ return;
+
// Time to nag
String caption = Translator.INSTANCE.translate("SysTray", "NTP_NAG_CAPTION");
- String text = Translator.INSTANCE.translate("SysTray", "NTP_NAG_TEXT");
- SysTray.getInstance().showMessage(caption, text, MessageType.INFO);
+ String text = Translator.INSTANCE.translate("SysTray", isWindows ? "NTP_NAG_TEXT_WINDOWS" : "NTP_NAG_TEXT_UNIX");
+ SysTray.getInstance().showMessage(caption, text, MessageType.WARNING);
}
public void updateSysTray() {
@@ -467,7 +473,9 @@ public class Controller extends Thread {
final int height = getChainHeight();
- String tooltip = String.format("qora-core - %d peer%s - height %d", numberOfPeers, (numberOfPeers != 1 ? "s" : ""), height);
+ String connectionsText = Translator.INSTANCE.translate("SysTray", numberOfPeers != 1 ? "CONNECTIONS" : "CONNECTION");
+ String heightText = Translator.INSTANCE.translate("SysTray", "BLOCK_HEIGHT");
+ String tooltip = String.format("qora-core - %d %s - %s %d", numberOfPeers, connectionsText, heightText, height);
SysTray.getInstance().setToolTipText(tooltip);
}
diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java
index 9ceb1840..741416ff 100644
--- a/src/main/java/org/qora/settings/Settings.java
+++ b/src/main/java/org/qora/settings/Settings.java
@@ -95,6 +95,20 @@ public class Settings {
"https://raw.githubusercontent.com@151.101.16.133/catbref/qora-core/%s/qora-core.jar"
};
+ // NTP sources
+ private String[] ntpServers = new String[] {
+ "pool.ntp.org",
+ "0.pool.ntp.org",
+ "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"
+ };
+
// Constructors
private Settings() {
@@ -308,4 +322,8 @@ public class Settings {
return this.autoUpdateRepos;
}
+ public String[] getNtpServers() {
+ return this.ntpServers;
+ }
+
}
diff --git a/src/main/java/org/qora/utils/NTP.java b/src/main/java/org/qora/utils/NTP.java
new file mode 100644
index 00000000..414f2fc8
--- /dev/null
+++ b/src/main/java/org/qora/utils/NTP.java
@@ -0,0 +1,99 @@
+package org.qora.utils;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.net.ntp.NTPUDPClient;
+import org.apache.commons.net.ntp.TimeInfo;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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
+
+ /**
+ * Returns aggregated internet time.
+ *
+ * @return internet time (ms), or null if unsuccessful.
+ */
+ public static Long getTime() {
+ Long meanOffset = getOffset();
+ if (meanOffset == null)
+ return null;
+
+ return System.currentTimeMillis() + meanOffset;
+ }
+
+ /**
+ * Returns mean offset from internet time.
+ *
+ * Positive offset means local clock is behind internet time.
+ *
+ * @return offset (ms), or null if unsuccessful.
+ */
+ public static Long getOffset() {
+ String[] ntpServers = Settings.getInstance().getNtpServers();
+
+ NTPUDPClient client = new NTPUDPClient();
+ client.setDefaultTimeout(2000);
+
+ List offsets = new ArrayList<>();
+
+ for (String server : ntpServers) {
+ try {
+ TimeInfo timeInfo = client.getTime(InetAddress.getByName(server));
+
+ timeInfo.computeDetails();
+
+ LOGGER.debug(() -> String.format("%c%16.16s %16.16s %2d %c %4d %4d %3o %6dms % 5dms % 5dms",
+ ' ',
+ server,
+ timeInfo.getMessage().getReferenceIdString(),
+ timeInfo.getMessage().getStratum(),
+ 'u',
+ 0,
+ 1 << timeInfo.getMessage().getPoll(),
+ 1,
+ timeInfo.getDelay(),
+ timeInfo.getOffset(),
+ 0
+ ));
+
+ offsets.add((double) timeInfo.getOffset());
+ } catch (IOException e) {
+ // Try next server...
+ }
+ }
+
+ if (offsets.size() < ntpServers.length / 2) {
+ LOGGER.debug("Not enough replies");
+ return null;
+ }
+
+ // sₙ represents sum of offsetⁿ
+ double s0 = 0;
+ double s1 = 0;
+ double s2 = 0;
+
+ for (Double offset : offsets) {
+ s0 += 1;
+ s1 += offset;
+ s2 += offset * offset;
+ }
+
+ double mean = s1 / s0;
+ 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)
+ return null;
+
+ return (long) mean;
+ }
+
+}
diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties
index d161acd0..be493d1b 100644
--- a/src/main/resources/i18n/SysTray_en.properties
+++ b/src/main/resources/i18n/SysTray_en.properties
@@ -1,14 +1,22 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
+BLOCK_HEIGHT = height
+
CHECK_TIME_ACCURACY = Check time accuracy
+CONNECTION = block
+
+CONNECTIONS = connections
+
EXIT = Exit
# Nagging about lack of NTP time sync
-NTP_NAG_CAPTION = No connections?
+NTP_NAG_CAPTION = Computer's clock is inaccurate!
-NTP_NAG_TEXT = Please enable Windows automatic time synchronization
+NTP_NAG_TEXT_UNIX = Install NTP service to get an accurate clock.
+
+NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
OPEN_NODE_UI = Open Node UI
diff --git a/src/main/resources/i18n/SysTray_zh.properties b/src/main/resources/i18n/SysTray_zh.properties
index ff161d3d..d3349242 100644
--- a/src/main/resources/i18n/SysTray_zh.properties
+++ b/src/main/resources/i18n/SysTray_zh.properties
@@ -1,14 +1,20 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
+BLOCK_HEIGHT = \u5757\u9AD8\u5EA6
+
CHECK_TIME_ACCURACY = \u68C0\u67E5\u65F6\u95F4\u51C6\u786E\u6027
+CONNECTIONS = \u4E2A\u8FDE\u63A5
+
EXIT = \u9000\u51FA\u8F6F\u4EF6
# Nagging about lack of NTP time sync
-NTP_NAG_CAPTION = \u6CA1\u6709\u8FDE\u63A5\u4E0A\u8282\u70B9\uFF1F
+NTP_NAG_CAPTION = \u7535\u8111\u7684\u65F6\u949F\u4E0D\u51C6\u786E\uFF01
-NTP_NAG_TEXT = \u8BF7\u542F\u7528Windows\u81EA\u52A8\u65F6\u95F4\u540C\u6B65\u3002
+NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7684\u65F6\u949F\u3002
+
+NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
OPEN_NODE_UI = \u5F00\u542F\u754C\u9762
diff --git a/src/test/java/org/qora/test/NTPTests.java b/src/test/java/org/qora/test/NTPTests.java
new file mode 100644
index 00000000..f5f73236
--- /dev/null
+++ b/src/test/java/org/qora/test/NTPTests.java
@@ -0,0 +1,86 @@
+package org.qora.test;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.net.ntp.NTPUDPClient;
+import org.apache.commons.net.ntp.NtpV3Packet;
+import org.apache.commons.net.ntp.TimeInfo;
+
+public class NTPTests {
+
+ private static final List CC_TLDS = Arrays.asList("oceania", "europe", "lat", "asia", "africa");
+
+ public static void main(String[] args) throws UnknownHostException, IOException {
+ NTPUDPClient client = new NTPUDPClient();
+ client.setDefaultTimeout(2000);
+
+ System.out.println(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
+ ' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
+ ));
+
+ List offsets = new ArrayList<>();
+
+ List ntpServers = new ArrayList<>();
+ for (String ccTld : CC_TLDS) {
+ ntpServers.add(ccTld + ".pool.ntp.org");
+ for (int subpool = 0; subpool <=3; ++subpool)
+ ntpServers.add(subpool + "." + ccTld + ".pool.ntp.org");
+ }
+
+ for (String server : ntpServers) {
+ try {
+ TimeInfo timeInfo = client.getTime(InetAddress.getByName(server));
+
+ timeInfo.computeDetails();
+ NtpV3Packet ntpMessage = timeInfo.getMessage();
+
+ System.out.println(String.format("%c%16.16s %16.16s %2d %c %4d %4d %3o %6dms % 5dms % 5dms",
+ ' ',
+ server,
+ ntpMessage.getReferenceIdString(),
+ ntpMessage.getStratum(),
+ 'u',
+ 0,
+ 1 << ntpMessage.getPoll(),
+ 1,
+ timeInfo.getDelay(),
+ timeInfo.getOffset(),
+ 0
+ ));
+
+ offsets.add((double) timeInfo.getOffset());
+ } catch (IOException e) {
+ // Try next server...
+ }
+ }
+
+ if (offsets.size() < ntpServers.size() / 2) {
+ System.err.println("Not enough replies");
+ System.exit(1);
+ }
+
+ double s0 = 0;
+ double s1 = 0;
+ double s2 = 0;
+
+ for (Double offset : offsets) {
+ // Exclude nearby results for more extreme testing
+ if (offset < 100.0)
+ continue;
+
+ s0 += 1;
+ s1 += offset;
+ s2 += offset * offset;
+ }
+
+ double mean = s1 / s0;
+ double stddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
+ System.out.println(String.format("mean: %7.3f, stddev: %7.3f", mean, stddev));
+ }
+
+}