NTP and performance changes + fixes.

New NTP class now runs as a simplistic NTP client, repeatedly polling
several NTP servers and maintaining a more accurate time independent
of operating system.

Several occurrences of System.currentTimeMillis() replaced with NTP.getTime()
particularly where block/transaction/networking is involved.

GET /admin/info now includes "currentTimestamp" as reported from NTP.

Added support for block timestamps determined by generator, instead of
supplied by clock. (BlockChain.newBlockTimestampHeight - not yet activated).
Incorrect timestamps will produce a TIMESTAMP_INCORRECT Block.ValidationResult.

Block.calcMinimumTimestamp repurposed as Block.calcTimestamp for above.

Block timestamps are now allowed to be max 2000ms in the future,
was previously max 500ms.

Block generation prohibited until initial NTP sync.

Instead of deleting INVALID unconfirmed transactions in BlockGenerator,
Controller now deletes EXPIRED unconfirmed transactions every so often.
This also fixes persistent expired unconfirmed transactions on nodes
that do not generate blocks, as BlockGenerator.deleteInvalidTransactions()
was never reached.

Abbreviated block sigs added to log entries declaring a new block is generated
in BlockGenerator.

Controller checks for NTP sync much faster during start-up and SysTray's
tooltip text starts as "Synchronizing clock" until NTP sync occurs.
After NTP sync, Controller logs NTP offset every so often (currently every 5 mins).

When considering synchronizing, Controller skips peers that have the same block sig
as last time when synchronization resulted in no action, e.g. INFERIOR_CHAIN,
NOTHING_TO_DO and also OK. OK is included as another sync attempt would result in
NOTHING_TO_DO.
Previously this skipping check only happened after prior INFERIOR_CHAIN.

During inbound peer handshaking, if we receive a peer ID that matches an existing inbound
peer then send peer ID of all zeros, then close connection.
Remote end should detect this and cleanly close connection instead of waiting for handshake timeout.
Randomly generated peer IDs have lowest bit set to avoid all zeros.
Might need further work.

Networking doesn't connect, or accept, until NTP has synced.

Transaction validation can fail with CLOCK_NOT_SYNCED if NTP not synced.
This commit is contained in:
catbref
2019-07-31 16:08:22 +01:00
parent 05e491f65b
commit 63b262a76e
15 changed files with 605 additions and 240 deletions

View File

@@ -5,7 +5,13 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Executors;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.NtpV3Packet;
@@ -13,74 +19,182 @@ import org.apache.commons.net.ntp.TimeInfo;
public class NTPTests {
private static final List<String> CC_TLDS = Arrays.asList("oceania", "europe", "lat", "asia", "africa");
private static final List<String> CC_TLDS = Arrays.asList("oceania", "europe", "cn", "asia", "africa");
public static void main(String[] args) throws UnknownHostException, IOException {
public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
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"
));
class NTPServer {
private static final int MIN_POLL = 8;
List<Double> offsets = new ArrayList<>();
public char usage = ' ';
public String remote;
public String refId;
public Integer stratum;
public char type = 'u'; // unicast
public int poll = MIN_POLL;
public byte reach = 0;
public Long delay;
public Double offset;
public Double jitter;
List<String> 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");
}
private Deque<Double> offsets = new LinkedList<>();
private double totalSquareOffsets = 0.0;
private long nextPoll;
private Long lastGood;
for (String server : ntpServers) {
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(server));
public NTPServer(String remote) {
this.remote = remote;
}
timeInfo.computeDetails();
NtpV3Packet ntpMessage = timeInfo.getMessage();
public boolean poll(NTPUDPClient client) {
final long now = System.currentTimeMillis();
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
));
if (now < this.nextPoll)
return false;
offsets.add((double) timeInfo.getOffset());
} catch (IOException e) {
// Try next server...
boolean isUpdated = false;
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(remote));
timeInfo.computeDetails();
NtpV3Packet ntpMessage = timeInfo.getMessage();
this.refId = ntpMessage.getReferenceIdString();
this.stratum = ntpMessage.getStratum();
this.poll = Math.max(MIN_POLL, 1 << ntpMessage.getPoll());
this.delay = timeInfo.getDelay();
this.offset = (double) timeInfo.getOffset();
if (this.offsets.size() == 8) {
double oldOffset = this.offsets.removeFirst();
this.totalSquareOffsets -= oldOffset * oldOffset;
}
this.offsets.addLast(this.offset);
this.totalSquareOffsets += this.offset * this.offset;
this.jitter = Math.sqrt(this.totalSquareOffsets / this.offsets.size());
this.reach = (byte) ((this.reach << 1) | 1);
this.lastGood = now;
isUpdated = true;
} catch (IOException e) {
this.reach <<= 1;
}
this.nextPoll = now + this.poll * 1000;
return isUpdated;
}
public Integer getWhen() {
if (this.lastGood == null)
return null;
return (int) ((System.currentTimeMillis() - this.lastGood) / 1000);
}
}
if (offsets.size() < ntpServers.size() / 2) {
System.err.println("Not enough replies");
System.exit(1);
List<NTPServer> ntpServers = new ArrayList<>();
for (String ccTld : CC_TLDS)
for (int subpool = 0; subpool <=3; ++subpool)
ntpServers.add(new NTPServer(subpool + "." + ccTld + ".pool.ntp.org"));
while (true) {
Thread.sleep(1000);
CompletionService<Boolean> ecs = new ExecutorCompletionService<Boolean>(Executors.newCachedThreadPool());
for (NTPServer server : ntpServers)
ecs.submit(() -> server.poll(client));
boolean showReport = false;
for (int i = 0; i < ntpServers.size(); ++i)
try {
showReport = ecs.take().get() || showReport;
} catch (ExecutionException e) {
// skip
}
if (showReport) {
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null) {
server.usage = ' ';
continue;
}
server.usage = '+';
double value = server.offset * (double) server.stratum;
s0 += 1;
s1 += value;
s2 += value * value;
}
if (s0 < ntpServers.size() / 3 + 1) {
System.out.println("Not enough replies to calculate network time");
} else {
double filterStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
double filterMean = s1 / s0;
// Now only consider offsets within 1 stddev?
s0 = 0;
s1 = 0;
s2 = 0;
for (NTPServer server : ntpServers) {
if (server.offset == null || server.reach == 0)
continue;
if (Math.abs(server.offset * (double)server.stratum - filterMean) > filterStddev)
continue;
server.usage = '*';
s0 += 1;
s1 += server.offset;
s2 += server.offset * server.offset;
}
if (s0 <= 1) {
System.out.println(String.format("Not enough values to calculate network time. stddev: %7.4f", filterStddev));
} else {
double mean = s1 / s0;
double newStddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
System.out.println(String.format("filtering stddev: %7.3f, mean: %7.3f, new stddev: %7.3f, nValues: %.0f / %d", filterStddev, mean, newStddev, s0, ntpServers.size()));
}
}
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"
));
for (NTPServer server : ntpServers)
System.out.println(String.format("%c%16.16s %16.16s %2s %c %4s %4d %3o %7s %7s %7s",
server.usage,
server.remote,
formatNull("%s", server.refId, ""),
formatNull("%2d", server.stratum, ""),
server.type,
formatNull("%4d", server.getWhen(), "-"),
server.poll,
server.reach,
formatNull("%5dms", server.delay, ""),
formatNull("% 5.0fms", server.offset, ""),
formatNull("%5.2fms", server.jitter, "")
));
}
}
}
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));
private static String formatNull(String format, Object arg, String nullOutput) {
return arg != null ? String.format(format, arg) : nullOutput;
}
}