RFC 1305

Network Time Protocol (Version 3) Specification, Implementation and Analysis

๐Ÿ“… published March 1992, from ietf

Network Time Protocol (Version 3) ๐Ÿ’–

Specification, Implementation and Analysis โœจ

okay so this one is RFC 1305, and it's giving main character ๐Ÿ‘‘ It's by David L. Mills at the University of Delaware, it came out in March 1992 ๐ŸŽ€ and it's from the Network Working Group. It's an IAB standards track protocol, and it obsoletes RFC-1119, RFC-1059, and RFC-958 ๐ŸŒธ

Heads up, bestie: this is an approximate ASCII rendering of the PostScript original of the same name ๐Ÿ™ˆ It's here for convenience and for searching. Most tables, figures, equations and captions did NOT make it, and the pagination and section headings aren't available.

Abstract ๐Ÿฆ‹

this doc describes the Network Time Protocol (NTP), specifies its formal structure, and sums up the info you need to implement it ๐Ÿ’… NTP handles syncing time and coordinating how time gets distributed across a big, diverse internet, whether the links are mundane or lightwave-fast โšก It uses a returnable-time design, where a distributed subnet of time servers organizes itself into a hierarchical master-slave setup ๐Ÿงš Those servers keep local clocks synced with each other and with national time standards, over wire or radio. They can also pass reference time along using local routing algorithms and time daemons โœจ

Status of this Memo ๐ŸŽ€

this RFC specifies an IAB standards track protocol for the Internet community, and it's asking for discussion and suggestions for improvements ๐Ÿ’Œ For the standardization state and status of this protocol, check the current edition of "IAB Official Protocol Standards" ๐Ÿ“š Distribution of this memo is unlimited ๐ŸŒˆ

Keywords: network clock synchronization, standard time distribution, fault-tolerant architecture, maximum-likelihood estimation, disciplined oscillator, internet protocol, high-speed networks, formal specification ๐Ÿ”ฅ

Preface ๐Ÿงš

okay so this document describes Version 3 of NTP ๐Ÿ’– It supersedes Version 2, which was described in RFC-1119 from September 1989. BUT, and this is important ๐Ÿซถ, it doesn't change the protocol in any significant way, and it doesn't obsolete previous versions or existing implementations. The big motivation for the new version is to sharpen the analysis and implementation models for new apps running at much higher network speeds, all the way up to the gigabit-per-second regime ๐Ÿš€ It also gets the stability, accuracy, and precision that speeds like that require โœจ The sources of time and frequency errors were rigorously examined, and error bounds were established. That helps performance, gives a model for correctness assertions, and lets the user see how good their timekeeping is ๐Ÿ‘‘

The revision also adds two new optional features ๐Ÿ“ Feature (1) is an algorithm that combines the offsets of several peer time servers to boost accuracy. Feature (2) is improved local-clock algorithms, which let the poll intervals on all synchronization paths get substantially longer, so there's less network overhead ๐Ÿฆ‹

Here's the overview of the changes, which are described in detail in Appendix D ๐Ÿ’…

  1. In Version 3 the local-clock algorithm got a full glow-up for stability and accuracy ๐Ÿ’– Appendix G has a detailed mathematical model and a design example, refined with feedback-control analysis and lots of simulation using data from ordinary Internet paths ๐Ÿงช Section 5 of RFC-1119, which covered the NTP local clock, was completely rewritten to describe the new algorithm ๐ŸŽ€ Since the new algorithm can produce message rates way below the old ones, it is highly recommended that it be used in new implementations โœจ Using it doesn't affect interoperability with previous versions or existing implementations ๐Ÿซถ
  2. In Version 3 there's a new algorithm for combining the offsets of several peer time servers, presented in Appendix F ๐Ÿ”ฅ It's modeled on the ones national standards labs use to combine the weighted offsets from several standard clocks, building a synthetic laboratory timescale that's more accurate than any single clock ๐Ÿ‘‘ It can be used in an NTP implementation to improve accuracy and stability, and to reduce errors caused by asymmetric paths on the Internet ๐ŸŒˆ The algorithm was simulated on data from ordinary Internet paths and, along with the new local-clock algorithm, was implemented and tested in the Fuzzball time servers running on the Internet right now ๐Ÿงš Again, using it doesn't affect interoperability with previous versions or existing implementations ๐Ÿ’œ
  3. Several inconsistencies and minor errors from previous versions got fixed in Version 3 ๐Ÿ™ˆ The procedures were rewritten as pseudo-code with English commentary, for clarity and to avoid ambiguity ๐Ÿ“š Appendix I was added with C-language implementations of the various filtering and selection algorithms suggested for NTP ๐Ÿ’ป There's extra info in Section 5 and in Appendix E. Appendix E includes the tutorial material that used to live in Section 2 of RFC-1119, plus lots of new stuff clarifying how timescales and leap seconds get interpreted ๐Ÿฆ‹
  4. Some minor changes were made to the Version 3 local-clock algorithms ๐ŸŽ€ They avoid problems seen when leap seconds get introduced into the UTC timescale, and they add support for an auxiliary precision oscillator, like a cesium clock or timing receiver, as a precision timebase โฑ๏ธ Changes were also made to some procedures in Section 3, and to the clock-filter and clock-selection procedures in Section 4 ๐Ÿ’… These fixed minor bugs found through experience, and they're recommended for new implementations. They don't affect interoperability with previous versions or existing implementations in any way beyond minor ones (at least until the next leap second) ๐ŸŒธ
  5. In Version 3 the way delay, offset, and dispersion are defined, calculated, and processed changed, so that the errors inherent in time-transfer procedures can be reliably bounded ๐Ÿ”ฅ The error accumulations moved out of the delay computation and into the dispersion computation, and both are included in the clock filter and selection procedures โœจ The clock-selection procedure dropped the first of its two sorting/discarding steps and swapped in an algorithm first proposed by Marzullo and later adopted in the Digital Time Service ๐Ÿง  These changes don't significantly affect ordinary operation or compatibility with the various NTP versions, but they do give the basis for the formal statements of correctness in Appendix H ๐Ÿ’–

Table of Contents ๐Ÿ“‹

1.       Introduction   1

1.1.     Related Technology     2

2.       System Architecture    4

2.1.     Implementation Model   6

2.2.     Network Configurations 7

3.       Network Time Protocol  8

3.1.     Data Formats   8

3.2.     State Variables and Parameters 9

3.2.1.   Common Variables       9

3.2.2.   System Variables       12

3.2.3.   Peer Variables 12

3.2.4.   Packet Variables       14

3.2.5.   Clock-Filter Variables 14

3.2.6.   Authentication Variables       15

3.2.7.   Parameters     15

3.3.     Modes of Operation     17

3.4.     Event Processing       19

3.4.1.   Notation Conventions   19

3.4.2.   Transmit Procedure     20

3.4.3.   Receive Procedure      22

3.4.4.   Packet Procedure       24

3.4.5.   Clock-Update Procedure 27

3.4.6.   Primary-Clock Procedure        28

3.4.7.   Initialization Procedures      28

3.4.7.1.         Initialization Procedure       29

3.4.7.2.         Initialization-Instantiation Procedure 29

3.4.7.3.         Receive-Instantiation Procedure        30

3.4.7.4.         Primary Clock-Instantiation Procedure  31

3.4.8.   Clear Procedure        31

3.4.9.   Poll-Update Procedure  32

3.5.     Synchronization Distance Procedure     32

3.6.     Access Control Issues  33

4.       Filtering and Selection Algorithms     34

4.1.     Clock-Filter Procedure 35

4.2.     Clock-Selection Procedure      36

4.2.1.   Intersection Algorithm 36

5.       Local Clocks   40

5.1.     Fuzzball Implementation        41

5.2.     Gradual Phase Adjustments      42

5.3.     Step Phase Adjustments 43

5.4.     Implementation Issues  44

6.       Acknowledgments        45

7.       References     46

A.       Appendix A. NTP Data Format - Version 3        50

B.       Appendix B. NTP Control Messages       53

B.1.     NTP Control Message Format     54

B.2.     Status Words   56

B.2.1.   System Status Word     56

B.2.2.   Peer Status Word       57

B.2.3.   Clock Status Word      58

B.2.4.   Error Status Word      58

B.3.     Commands       59

C.       Appendix C. Authentication Issues      61

C.1.     NTP Authentication Mechanism   62

C.2.     NTP Authentication Procedures  63

C.2.1.   Encrypt Procedure      63

4.2.2.   Clustering Algorithm   38

C.2.2.   Decrypt Procedure      64

C.2.3.   Control-Message Procedures     65

D.       Appendix D. Differences from Previous Versions.        66

E.       Appendix E. The NTP Timescale and its Chronometry      70

E.1.     Introduction   70

E.2.     Primary Frequency and Time Standards   70

E.3.     Time and Frequency Dissemination       72

E.4.     Calendar Systems       74

E.5.     The Modified Julian Day System 75

E.6.     Determination of Frequency     76

E.7.     Determination of Time and Leap Seconds 76

E.8.     The NTP Timescale and Reckoning with UTC       78

F.       Appendix F. The NTP Clock-Combining Algorithm  80

F.1.     Introduction   80

F.2.     Determining Time and Frequency 80

F.3.     Clock Modelling        81

F.4.     Development of a Composite Timescale   81

F.5.     Application to NTP     84

F.6.     Clock-Combining Procedure      84

G.       Appendix G. Computer Clock Modelling and Analysis      86

G.1.     Computer Clock Models  86

G.1.1.   The Fuzzball Clock Model       88

G.1.2.   The Unix Clock Model   89

G.2.     Mathematical Model of the NTP Logical Clock    91

G.3.     Parameter Management   93

G.4.     Adjusting VCO Gain (<$Ebold alpha>)    94

G.5.     Adjusting PLL Bandwidth (<$Ebold tau>) 94

G.6.     The NTP Clock Model    95

H.       Appendix H. Analysis of Errors and Correctness Principles

98

H.1.     Introduction   98

H.2.     Timestamp Errors       98

H.3.     Measurement Errors     100

H.4.     Network Errors 101

H.5.     Inherited Errors       102

H.6.     Correctness Principles 104

I.       Appendix I. Selected C-Language Program Listings       107

I.1.     Common Definitions and Variables       107

I.2.     Clock<196>Filter Algorithm     108

I.3.     Interval Intersection Algorithm        109

I.4.     Clock<196>Selection Algorithm  110

I.5.     Clock<196>Combining Procedure  111

I.6.     Subroutine to Compute Synchronization Distance 112

List of Figures ๐Ÿ–ผ๏ธ

Figure 1. Implementation Model  6

Figure 2. Calculating Delay and Offset  25

Figure 3. Clock Registers       39

Figure 4. NTP Message Header    50

Figure 5. NTP Control Message Header    54

Figure 6. Status Word Formats   55

Figure 7. Authenticator Format  63

Figure 8. Comparison of UTC and NTP Timescales at Leap  79

Figure 9. Network Time Protocol 80

Figure 10. Hardware Clock Models        86

Figure 11. Clock Adjustment Process     90

Figure 12. NTP Phase-Lock Loop (PLL) Model      91

Figure 13. Timing Intervals     96

Figure 14. Measuring Delay and Offset   100

Figure 15. Error Accumulations  103

Figure 16. Confidence Intervals and Intersections       105

List of Tables ๐Ÿงพ

Table 1. System Variables       12

Table 2. Peer Variables 13

Table 3. Packet Variables       14

Table 4. Parameters     16

Table 5. Modes and Actions      22

Table 6. Clock Parameters       40

Table 7. Characteristics of Standard Oscillators        71

Table 8. Table of Leap-Second Insertions        77

Table 9. Notation Used in PLL Analysis  91

Table 10. PLL Parameters        91
Table 11. Notation Used in PLL Analysis 95

Table 12. Notation Used in Error Analysis       98

Introduction โœจ

Okay bestie, so this doc is the formal spec for Network Time Protocol (NTP) Version 3 ๐Ÿ•ฐ๏ธ. NTP keeps timekeeping in sync across a whole set of distributed time servers and clients, and it's mainly written for implementors ๐Ÿ’ป. There's a companion doc [MIL91a] that sums up the requirements, analytical models, algorithmic analysis and performance under typical Internet conditions ๐Ÿ“Š. Another doc, [MIL91b], explains the NTP timescale and how it relates to the other standard timescales in use ๐Ÿ—“๏ธ. NTP was first described in RFC-958 [MIL85c], but it has glowed up a lot since then, culminating in the most recent NTP Version 2, described in RFC-1119 [MIL89] ๐Ÿ’…. It's built on the Internet Protocol (IP) [DAR81a] and User Datagram Protocol (UDP) [POS80], which give it a connectionless transport mechanism, but it's readily adaptable to other protocol suites ๐Ÿฆ‹. NTP evolved from the Time Protocol [POS83b] and the ICMP Timestamp message [DAR81b], but it's specifically designed to stay accurate and robust even over typical Internet paths with multiple gateways, highly dispersive delays and unreliable nets ๐Ÿ”ฅ.

The service environment is made up of the implementation model and service model described in Section 2 ๐Ÿงฉ. The implementation model is based on a multiple-process operating system architecture, although other architectures could be used as well. The service model is a returnable-time design which depends only on measured clock offsets, but it does not require reliable message delivery ๐Ÿซถ. The synchronization subnet uses a self-organizing, hierarchical-master-slave configuration, with synchronization paths determined by a minimum-weight spanning tree ๐ŸŒณ. Multiple masters (primary servers) may exist, but there is no requirement for an election protocol ๐Ÿ‘‘.

NTP itself is described in Section 3 ๐Ÿ’–. It provides the protocol mechanisms to synchronize time in principle to precisions in the order of nanoseconds, while preserving a non-ambiguous date well into the next century โฐโœจ. The protocol includes provisions to specify the characteristics and estimate the error of the local clock and of the time server it may be synchronized to. It also includes provisions for operation with a number of mutually suspicious, hierarchically distributed primary reference sources, such as radio-synchronized clocks ๐Ÿ“ป.

Section 4 describes algorithms useful for deglitching and smoothing clock-offset samples collected on a continuous basis ๐Ÿงผ. These algorithms evolved from those suggested in [MIL85a], were refined as the results of experiments described in [MIL85b], and further evolved under typical operating conditions over the last three years ๐Ÿงช. Plus, thanks to experience running multiple-server subnets, including radio clocks at several sites in the U.S. and with clients in the U.S. and Europe, reliable algorithms for selecting good clocks from a population that might include broken ones have been developed [DEC89], [MIL91a] and are described in Section 4 ๐Ÿ’ฏ.

The accuracies NTP can achieve depend strongly on the precision of the local-clock hardware and stringent control of device and process latencies. Provisions must be included to adjust the software logical-clock time and frequency in response to corrections produced by NTP ๐Ÿ› ๏ธ. Section 5 describes a local-clock design evolved from the Fuzzball implementation described in [MIL83b] and [MIL88b] ๐ŸŽ€. This design includes offset-slewing, frequency compensation and deglitching mechanisms, capable of accuracies in the order of a millisecond, even after extended periods when synchronization to primary reference sources has been lost ๐Ÿซ .

Details specific to NTP packet formats used with the Internet Protocol (IP) and User Datagram Protocol (UDP) are presented in Appendix A ๐Ÿ“ฆ. Details of a suggested auxiliary NTP Control Message, which may be used when comprehensive network-monitoring facilities are not available, are presented in Appendix B ๐Ÿ”. Appendix C contains specification and implementation details of an optional authentication mechanism, which can be used to control access and prevent unauthorized data modification ๐Ÿ”. Appendix D contains a listing of differences between Version 3 of NTP and previous versions ๐Ÿ“. Appendix E expands on the issues involved with precision timescales and calendar dating peculiar to computer networks and NTP ๐Ÿ“…. Appendix F describes an optional algorithm to improve accuracy by combining the time offsets of a number of clocks ๐Ÿงฎ. Appendix G presents a detailed mathematical model and analysis of the NTP local-clock algorithms ๐Ÿ“. Appendix H analyzes the sources and propagation of errors and presents correctness principles relating to the time-transfer service ๐Ÿ›ก๏ธ. Appendix I illustrates C-language code segments for the clock-filter, clock-selection and related algorithms described in Section 4 ๐Ÿ’ป๐ŸŒธ.

Other mechanisms have been specified in the Internet protocol suite to record and transmit the time at which an event takes place. These include the Daytime protocol [POS83a], Time Protocol [POS83b], ICMP Timestamp message [DAR81b] and IP Timestamp option [SU81] โฑ๏ธ. Experimental results on measured clock offsets and roundtrip delays in the Internet are discussed in [MIL83a], [MIL85b], [COL88] and [MIL88a] ๐Ÿ“ˆ. Other synchronization algorithms are discussed in [LAM78], [GUS84], [HAL84], [LUN84], [LAM85], [MAR85], [MIL85a], [MIL85b], [MIL85c], [GUS85b], [SCH86], [TRI86], [RIC88], [MIL88a], [DEC89] and [MIL91a], while protocols based on them are described in [MIL81a], [MIL81b], [MIL83b], [GUS85a], [MIL85c], [TRI86], [MIL88a], [DEC89] and [MIL91a] ๐Ÿ“š. NTP uses techniques evolved from them, along with both linear-systems and agreement methodologies. Linear methods for digital telephone network synchronization are summarized in [LIN80], while agreement methods for clock synchronization are summarized in [LAM85] ๐Ÿ“ž.

The Digital Time Service (DTS) [DEC89] has many of the same service objectives as NTP ๐Ÿข. The DTS design puts heavy emphasis on configuration management and correctness principles when operated in a managed LAN or LAN-cluster environment, while NTP puts heavy emphasis on the accuracy and stability of the service operated in an unmanaged, global-internet environment ๐ŸŒ. In DTS, a synchronization subnet consists of clerks, servers, couriers and time providers. With respect to the NTP nomenclature, a time provider is a primary reference source, a courier is a secondary server intended to import time from one or more distant primary servers for local redistribution, and a server is intended to provide time for possibly many end nodes or clerks. Unlike NTP, DTS does not need or use mode or stratum information in clock selection, and it does not include provisions to filter timing noise, select the most accurate from a set of presumed correct clocks or compensate for inherent frequency errors ๐Ÿ™ˆ.

In fact, the latest revisions in NTP have adopted certain features of DTS in order to support correctness principles ๐Ÿค. These include mechanisms to bound the maximum errors inherent in the time-transfer procedures, and the use of a provably correct (subject to stated assumptions) mechanism to reject inappropriate peers in the clock-selection procedures. These features are described in Section 4 and Appendix H of this document โœจ.

The Fuzzball routing protocol [MIL83b], sometimes called Hellospeak, incorporates time synchronization directly into the routing-protocol design ๐Ÿงš. One or more processes synchronize to an external reference source, such as a radio clock or NTP daemon, and the routing algorithm constructs a minimum-weight spanning tree rooted on these processes. The clock offsets are then distributed along the arcs of the spanning tree to all processes in the system, and the various process clocks are corrected using the procedure described in Section 5 of this document ๐ŸŒณ. While it can be seen that the design of Hellospeak strongly influenced the design of NTP, Hellospeak itself is not an Internet protocol and is unsuited for use outside its local-net environment ๐Ÿ .

The Unix 4.3bsd time daemon timed [GUS85a] uses a single master-time daemon to measure offsets of a number of slave hosts and send periodic corrections to them ๐Ÿ‘‘. In this model, the master is determined using an election algorithm [GUS85b] designed to avoid situations where either no master is elected or more than one master is elected. The election process requires a broadcast capability, which is not a ubiquitous feature of the Internet ๐Ÿ“ก. While this model has been extended to support hierarchical configurations, in which a slave on one network serves as a master on the other [TRI86], the model requires handcrafted configuration tables in order to establish the hierarchy and avoid loops ๐Ÿงฉ. In addition to the burdensome, but presumably infrequent, overheads of the election process, the offset measurement/correction process requires twice as many messages as NTP per update ๐Ÿ“ฌ.

A scheme with features similar to NTP is described in [KOP87] ๐Ÿฆ‹. This scheme is intended for multi-server LANs, where each of a set of possibly many time servers determines its local-time offset relative to each of the other servers in the set using periodic timestamped messages, then determines the local-clock correction using the Fault-Tolerant Average (FTA) algorithm of [LUN84]. The FTA algorithm, which is useful where up to k servers may be faulty, sorts the offsets, discards the k highest and lowest ones and averages the rest. The scheme, as described in [SCH86], is most suitable to LAN environments which support broadcast, and would result in unacceptable overhead in an internet environment. In addition, for reasons given in Section 4 of this paper, the statistical properties of the FTA algorithm are not likely to be optimal in an internet environment with highly dispersive delays ๐Ÿค“.

A good deal of research has gone into the issue of maintaining accurate time in a community where some clocks cannot be trusted ๐Ÿฅบ. A truechimer is a clock that maintains timekeeping accuracy to a previously published (and trusted) standard, while a falseticker is a clock that does not. Determining whether a particular clock is a truechimer or a falseticker is an interesting abstract problem, which can be attacked using agreement methods summarized in [LAM85] and [SRI87] ๐Ÿง .

A convergence function operates upon the offsets between the clocks in a system to increase accuracy by reducing or eliminating errors caused by falsetickers ๐ŸŽฏ. There are two classes of convergence functions: those involving interactive-convergence algorithms and those involving interactive-consistency algorithms. Interactive-convergence algorithms use statistical clustering techniques such as the fault-tolerant average algorithm of [HAL84], the CNV algorithm of [LUN84], the majority-subset algorithm of [MIL85a], the non-Byzantine algorithm of [RIC88], the egocentric algorithm of [SCH86], the intersection algorithm of [MAR85] and [DEC89], and the algorithms in Section 4 of this document ๐Ÿ“Š.

Interactive-consistency algorithms are designed to detect faulty clock processes which might indicate grossly inconsistent offsets in successive readings or to different readers ๐Ÿšจ. These algorithms use an agreement protocol involving successive rounds of readings, possibly relayed and possibly augmented by digital signatures. Examples include the fireworks algorithm of [HAL84] and the optimum algorithm of [SRI87]. However, these algorithms require large numbers of messages, especially when large numbers of clocks are involved, and they are designed to detect faults that have rarely been found in Internet experience ๐Ÿ˜ญ. For these reasons, they are not considered further in this document ๐Ÿ™…โ€โ™€๏ธ.

In practice it is not possible to determine the truechimers from the falsetickers on other than a statistical basis, especially with

hierarchical setups on a statistically noisy Internet, ngl. Sure, you can bound the max errors in the time-transfer procedures if you pick super generous tolerances for the hardware, but that usually means pretty meh accuracy and stability ๐Ÿ˜ญ. NTP (and what came before it) takes a different route: mutually coupled oscillators, maximum-likelihood estimation, clock-selection procedures, and a design where you can make provable assertions about error bounds, as long as you state your assumptions about the primary reference sources being correct ๐Ÿ’…. Analytically, a bunch of distributed NTP peers act like coupled phase-locked oscillators: the update algorithm is the phase detector, the local clock is the disciplined oscillator, and error bounds get calculated deterministically at every step of the time-transfer process โœจ.

The particular offset measurement and computation procedure described in Section 3 is a variant of the returnable-time system used in some digital telephone networks [LIN80] ๐Ÿ“ž. The clock filter and selection algorithms are designed so the clock synchronization subnet self-organizes into a hierarchical-master-slave configuration [MIT80] ๐ŸŒณ. The similarity between NTP and digital telephone systems is not an accident, bestie, since those systems have been studied extensively [LIN80], [BRA80]. What makes the NTP model unique is the adaptive configuration, polling, filtering, selection and correctness mechanisms that tune the dynamics of the system to fit the ubiquitous Internet environment ๐Ÿฆ‹.

System Architecture ๐ŸŒธ

In the NTP model, a bunch of primary reference sources get synchronized by wire or radio to national standards, hooked up to widely accessible resources like backbone gateways, and run as primary time servers ๐Ÿ›๏ธ. The whole point of NTP is to carry timekeeping info from these servers to other time servers over the Internet, and also to cross-check clocks and clean up errors caused by equipment or propagation failures ๐Ÿงน. Some local-net hosts or gateways act as secondary time servers and run NTP with one or more of the primary servers. To cut down on protocol overhead, the secondary servers pass time along via NTP to the rest of the local-net hosts. For extra reliability, selected hosts can be equipped with less accurate but cheaper radio clocks, and used as backup in case the primary and/or secondary servers or the paths between them fail ๐Ÿ›Ÿ.

Throughout this document we use a standard nomenclature, so let's get the vocab straight ๐Ÿ“š. The stability of a clock is how well it can hold a constant frequency. The accuracy is how well its frequency and time match national standards. The precision is how precisely those quantities can be kept within a particular timekeeping system. Unless said otherwise, the offset of two clocks is the time difference between them, while the skew is the frequency difference between them (the first derivative of offset with time). Real clocks show some variation in skew (the second derivative of offset with time), and that's called drift. In this version of the spec, though, drift is assumed to be zero ๐Ÿ™ˆ.

NTP is designed to produce three products: clock offset, roundtrip delay and dispersion, all relative to a selected reference clock ๐ŸŽฏ. Clock offset tells you how much to adjust the local clock so it lines up with the reference clock. Roundtrip delay gives you the ability to launch a message so it arrives at the reference clock at a specified time. Dispersion is the maximum error of the local clock relative to the reference clock. Since most host time servers sync through another peer time server, each of these three products has two components: one determined by the peer relative to the primary reference source of standard time, and one measured by the host relative to the peer. These components get maintained separately in the protocol, which makes error control and subnet management way easier ๐Ÿง . They don't just give precise measurements of offset and delay, they also give definitive maximum error bounds, so the user interface can tell you not only what time it is but also how good that time is ๐Ÿ‘‘.

There's no provision for peer discovery or virtual-circuit management in NTP ๐Ÿ™…โ€โ™€๏ธ. Data integrity comes from the IP and UDP checksums. No flow-control or retransmission facilities are provided, or needed. Duplicate detection is built right into the processing algorithms. The service can run in a symmetric mode, where servers and clients are indistinguishable but keep a small amount of state info, or in client/server mode, where servers don't need to keep any state beyond what's in the client's request ๐Ÿ’–. A lightweight association-management capability, with dynamic reachability and variable poll-rate mechanisms, is only there to manage state info and cut down on resource requirements. Since there's only a single NTP message format, the protocol is super easy to implement, and it works with a variety of solicited or unsolicited polling mechanisms ๐ŸŽ€.

It's important to realize that clock synchronization takes long periods and lots of comparisons to keep timekeeping accurate โณ. A few measurements are usually enough to pin down local time to within a second or so, but you need many hours and dozens of measurements to resolve oscillator skew and keep local time to the order of a millisecond. So the accuracy you get is directly tied to how long you take to get it ๐Ÿข. The good news? The measurements can happen pretty infrequently, and they're almost always non-intrusive to normal net operations ๐Ÿซถ.

Implementation Model ๐Ÿงฉ

In what's probably the most common client/server model, a client sends an NTP message to one or more servers and processes the replies as they come in ๐Ÿ“จ. The server swaps addresses and ports, overwrites certain fields in the message, recalculates the checksum and sends the message right back, immediately โšก. The info in the NTP message lets the client figure out the server's time relative to its own local time and adjust the local clock accordingly. The message also includes info to estimate the expected timekeeping accuracy and reliability, and to pick the best option out of possibly several servers ๐Ÿ†.

The client/server model can be enough for local nets with a public server and maybe a bunch of workstation clients ๐Ÿ’ป. But the full generality of NTP needs distributed participation from a bunch of client/servers or peers, arranged in a dynamically reconfigurable, hierarchically distributed configuration ๐Ÿ•ธ๏ธ. It also needs sophisticated algorithms for association management, data manipulation and local-clock control ๐Ÿค“. For the rest of this document, the term host means an instance of the protocol on a local processor, while the term peer means an instance of the protocol on a remote processor connected by a network path ๐Ÿซง.

Figure 1 <$&fig1> shows an implementation model for a host with three processes sharing a partitioned database (one partition for each peer), all connected by a message-passing system ๐Ÿ—‚๏ธ. The transmit process is driven by independent timers for each peer. It collects info from the database and sends NTP messages to the peers. Each message carries the local timestamp at the moment it's sent, along with previously received timestamps and any other info needed to figure out the hierarchy and manage the association ๐Ÿช„. How often messages get sent depends on the accuracy the local clock needs, as well as the accuracies of its peers โฑ๏ธ.

The receive process takes in NTP messages, plus maybe messages from other protocols and info from directly connected radio clocks ๐Ÿ“ก. When an NTP message comes in, the offset between the peer clock and the local clock gets computed and stored in the database, along with other stuff useful for error determination and peer selection. A filtering algorithm, described in Section 4, boosts accuracy by tossing out the inferior data ๐Ÿ—‘๏ธโœจ.

The update procedure kicks off when a message is received, and at other times too ๐Ÿ’ซ. It processes the offset data from each peer and picks the best one using the algorithms in Section 4. That might mean lots of observations from a few peers, or a few observations from lots of peers, depending on how accurate you need to be ๐ŸŽฏ.

The local-clock process works on the offset data from the update procedure and adjusts the phase and frequency of the local clock using the mechanisms in Section 5 ๐Ÿ•ฐ๏ธ. That can mean either a step-change or a gradual phase adjustment, so the offset goes to zero. The local clock then acts as a stable source of time info for other users of the system, and for NTP's own future reference ๐Ÿ‘‘๐ŸŒˆ.

Network Configurations ๐ŸŒ

The synchronization subnet is a connected network made up of primary and secondary time servers, clients and the transmission paths connecting them ๐Ÿงต. A primary time server is directly synced to a primary reference source, usually a radio clock ๐Ÿ“ป. A secondary time server gets its sync, possibly through other secondary servers, from a primary server, over network paths that may be shared with other services ๐Ÿ›ค๏ธ. Under normal circumstances, the synchronization subnet is meant to form a hierarchical-master-slave configuration, with the primary servers at the root and secondary servers of decreasing accuracy at each successive level toward the leaves ๐ŸŒฟ.

Following conventions established by the telephone industry [BEL86], the accuracy of each server is defined by a number called the stratum ๐Ÿชœ. The topmost level (primary servers) is assigned stratum one, and each level further down (secondary servers) gets a stratum one greater than the level above it. With current technology and available radio clocks, single-sample accuracies on the order of a millisecond can be achieved at the network interface of a primary server ๐ŸŽฏ. Accuracies like that need special care in the design and implementation of the operating system and the local-clock mechanism, as described in Section 5 ๐Ÿ› ๏ธ.

As the stratum goes up from one, the single-sample accuracies you can get will degrade, depending on the network paths and local-clock stabilities ๐Ÿ“‰. To avoid the tedious calculations [BRA80] needed to estimate errors in every specific configuration, it's handy to assume the mean measurement errors add up roughly in proportion to the measured delay and dispersion relative to the root of the synchronization subnet ๐Ÿงฎ. Appendix H has an analysis of errors, including a derivation of maximum error as a function of delay and dispersion. The dispersion depends on the precision of the timekeeping system, the frequency tolerance of the local clock and various residuals ๐Ÿ”ฌ. As long as the primary servers are synced to standard time within known accuracies, this gives a reliable, deterministic specification of timekeeping accuracies throughout the synchronization subnet ๐Ÿ’ฏ.

Again taking a lesson from the telephone industry, which learned this stuff the hard way [ABA89], the synchronization subnet topology should be organized to produce the highest accuracy, but must never be allowed to form a loop ๐Ÿ”๐Ÿšซ. Another factor: every increment in stratum involves a potentially unreliable time server, which adds more measurement errors ๐Ÿ˜ฌ. The selection algorithm in NTP uses a variant of the Bellman-Ford distributed routing algorithm [37] to compute the minimum-weight spanning trees rooted on the primary

Servers, bestie ๐Ÿ’– The distance metric the algorithm uses is the (scaled) stratum plus the synchronization distance, and the synchronization distance is the dispersion plus one-half the absolute delay ๐Ÿงš So the synchronization path will always take the minimum number of servers to the root, with ties resolved on the basis of maximum error โœจ

As a result of this design, the subnet reconfigures automatically into a hierarchical-master-slave configuration to produce the most accurate and reliable time ๐Ÿ‘‘ It does this even when one or more primary or secondary servers, or the network paths between them, fail ๐Ÿ’… That includes the case where all the normal primary servers (for example, a highly accurate WWVB radio clock operating at the lowest synchronization distances) on a possibly partitioned subnet fail, but one or more backup primary servers (for example, a less accurate WWV radio clock operating at higher synchronization distances) keep running ๐Ÿฆ‹ However, should all primary servers throughout the subnet fail, the remaining secondary servers will synchronize among themselves while distances ratchet upwards to a preselected maximum infinity, due to the well-known properties of the Bellman-Ford algorithm ๐Ÿ˜ญ Once the maximum is reached on all paths, a server will drop off the subnet and free-run using its last determined time and frequency. Since these computations are expected to be very precise, especially in frequency, even extended outage periods can result in timekeeping errors not greater than a few milliseconds per day with appropriately stabilized oscillators (see Section 5) ๐ŸŒˆ

In the case of multiple primary servers, the spanning-tree computation will usually select the server at minimum synchronization distance ๐Ÿ“ However, when these servers are at approximately the same distance, the computation MAY result in random selections among them as the result of normal dispersive delays. Ordinarily, this does not degrade accuracy as long as any discrepancy between the primary servers is small compared to the synchronization distance. If not, the filter and selection algorithms will select the best of the available servers and cast out outlyers as intended ๐Ÿ™ˆ


Network Time Protocol โœจ

This section consists of a formal definition of the Network Time Protocol, including its data formats, entities, state variables, events and event-processing procedures ๐Ÿ’œ The specification is based on the implementation model illustrated in Figure 1, but it is not intended that this model is the only one upon which a specification can be based. In particular, the specification is intended to illustrate and clarify the intrinsic operations of NTP, as well as to serve as a foundation for a more rigorous, comprehensive and verifiable specification ๐ŸŽ€

Data Formats ๐ŸŽ€

All mathematical operations expressed or implied herein are in two's-complement, fixed-point arithmetic. Data are specified as integer or fixed-point quantities, with bits numbered in big-endian fashion from zero starting at the left, or high-order, position. Since various implementations MAY scale externally derived quantities for internal use, neither the precision nor the decimal-point placement for fixed-point quantities is specified. Unless specified otherwise, all quantities are unsigned and MAY occupy the full field width with an implied zero preceding bit zero. Hardware and software packages designed to work with signed quantities will thus yield surprising results when the most significant (sign) bit is set ๐Ÿ’€ It is suggested that externally derived, unsigned fixed-point quantities such as timestamps be shifted right one bit for internal use, since the precision represented by the full field width is seldom justified.

Since NTP timestamps are cherished data and, in fact, represent the main product of the protocol, a special timestamp format has been established ๐Ÿซถ NTP timestamps are represented as a 64-bit unsigned fixed-point number, in seconds relative to 0h on 1 January 1900. The integer part is in the first 32 bits and the fraction part is in the last 32 bits. This format allows convenient multiple-precision arithmetic and conversion to Time Protocol representation (seconds), but it does complicate the conversion to ICMP Timestamp message representation (milliseconds). The precision of this representation is about 200 picoseconds, which SHOULD be adequate for even the most exotic requirements โœจ

Timestamps are determined by copying the current value of the local clock to a timestamp when some significant event, such as the arrival of a message, occurs ๐Ÿฆ‹ In order to maintain the highest accuracy, it is important that this be done as close to the hardware or software driver associated with the event as possible. In particular, departure timestamps SHOULD be redetermined for each link-level retransmission. In some cases a particular timestamp MAY not be available, such as when the host is rebooted or the protocol first starts up. In these cases the 64-bit field is set to zero, indicating the value is invalid or undefined ๐Ÿ™ˆ

Note that since some time in 1968 the most significant bit (bit 0 of the integer part) has been set, and that the 64-bit field will overflow some time in 2036 ๐Ÿ˜ญ Should NTP be in use in 2036, some external means will be necessary to qualify time relative to 1900 and time relative to 2036 (and other multiples of 136 years). Timestamped data requiring such qualification will be so precious that appropriate means SHOULD be readily available ๐Ÿ’– There will exist an 200-picosecond interval, henceforth ignored, every 136 years when the 64-bit field will be zero and thus considered invalid.

State Variables and Parameters ๐Ÿ’…

Following is a summary of the various state variables and parameters used by the protocol ๐ŸŒธ They are separated into classes of system variables, which relate to the operating system environment and local-clock mechanism; peer variables, which represent the state of the protocol machine specific to each peer; packet variables, which represent the contents of the NTP message; and parameters, which represent fixed configuration constants for all implementations of the current version. For each class, the description of the variable is followed by its name and the procedure or value which controls it. Note that variables are in lower case, while parameters are in upper case. Additional details on formats and use are presented in later sections and Appendices.

Common Variables ๐Ÿฆ‹

The following variables are common to two or more of the system, peer and packet classes. Additional variables are specific to the OPTIONAL authentication mechanism as described in Appendix C. When necessary to distinguish between common variables of the same name, the variable identifier will be used.

Peer Address (peer.peeraddr, pkt.peeraddr), Peer Port (peer.peerport, pkt.peerport): These are the 32-bit Internet address and 16-bit port number of the peer.

Host Address (peer.hostaddr, pkt.hostaddr), Host Port (peer.hostport, pkt.hostport): These are the 32-bit Internet address and 16-bit port number of the host. They are included among the state variables to support multi-homing ๐Ÿงš

Leap Indicator (sys.leap, peer.leap, pkt.leap): This is a two-bit code warning of an impending leap second to be inserted in the NTP timescale. The bits are set before 23:59 on the day of insertion and reset after 00:00 on the following day. This causes the number of seconds (rollover interval) in the day of insertion to be increased or decreased by one. In the case of primary servers the bits are set by operator intervention, while in the case of secondary servers the bits are set by the protocol. The two bits, bit 0 and bit 1, respectively, are coded as follows:

Code  Meaning
00    no warning
01    last minute has 61 seconds
10    last minute has 59 seconds
11    alarm condition (clock not synchronized)

In all except the alarm condition (112), NTP itself does nothing with these bits, except pass them on to the time-conversion routines that are not part of NTP. The alarm condition occurs when, for whatever reason, the local clock is not synchronized, such as when first coming up or after an extended period when no primary reference source is available ๐Ÿšจ

Mode (peer.mode, pkt.mode): This is an integer indicating the association mode, with values coded as follows:

Value  Meaning
0      unspecified
1      symmetric active
2      symmetric passive
3      client
4      server
5      broadcast
6      reserved for NTP control messages
7      reserved for private use

Stratum (sys.stratum, peer.stratum, pkt.stratum): This is an integer indicating the stratum of the local clock, with values defined as follows:

Value    Meaning
0        unspecified
1        primary reference (e.g., calibrated atomic clock, radio clock)
2-255    secondary reference (via NTP)

For comparison purposes, a value of zero is considered greater than any other value. Note that the maximum value of the integer encoded as a packet variable is limited by the parameter NTP.MAXSTRATUM ๐Ÿ“

Poll Interval (sys.poll, peer.hostpoll, peer.peerpoll, pkt.poll): This is a signed integer indicating the minimum interval between transmitted messages, in seconds as a power of two. For instance, a value of six indicates a minimum interval of 64 seconds โญ

Precision (sys.precision, peer.precision, pkt.precision): This is a signed integer indicating the precision of the various clocks, in seconds to the nearest power of two. The value MUST be rounded to the next larger power of two; for instance, a 50-Hz (20 ms) or 60-Hz (16.67 ms) power-frequency clock would be assigned the value -5 (31.25 ms), while a 1000-Hz (1 ms) crystal-controlled clock would be assigned the value -9 (1.95 ms) ๐Ÿ”ฅ

Root Delay (sys.rootdelay, peer.rootdelay, pkt.rootdelay): This is a signed fixed-point number indicating the total roundtrip delay to the primary reference source at the root of the synchronization subnet, in seconds. Note that this variable can take on both positive and negative values, depending on clock precision and skew ๐ŸŒˆ

Root Dispersion (sys.rootdispersion, peer.rootdispersion, pkt.rootdispersion): This is a signed fixed-point number indicating the maximum error relative to the primary reference source at the root of the synchronization subnet, in seconds. Only positive values greater than zero are possible ๐Ÿ’œ

Reference Clock Identifier (sys.refid, peer.refid, pkt.refid): This is a 32-bit code identifying the particular reference clock. In the case of stratum 0 (unspecified) or stratum 1 (primary reference source), this is a four-octet, left-justified, zero-padded ASCII string. For example (see Appendix A for the comprehensive list):

Stratum  Code  Meaning
0        DCN   DCN routing protocol
0        TSP   TSP time protocol
1        ATOM  Atomic clock (calibrated)
1        WWVB  WWVB LF (band 5) radio
1        GOES  GOES UHF (band 9) satellite
1        WWV   WWV HF (band 7) radio

In the case of stratum 2 and greater (secondary reference), this is the four-octet Internet address of the peer selected for synchronization ๐Ÿฆ‹

Reference Timestamp (sys.reftime, peer.reftime, pkt.reftime): This is the local time, in timestamp format, when the local clock was last

updated, bestie ๐ŸŒธ If the local clock has never been synchronized, the value is zero ๐Ÿซถ

Originate Timestamp (peer.org, pkt.org): okay so this is the local time, in timestamp format, at the peer when its latest NTP message was sent ๐Ÿฆ‹ If the peer becomes unreachable, the value is set to zero ๐Ÿ’€

Receive Timestamp (peer.rec, pkt.rec): this is the local time, in timestamp format, when the latest NTP message from the peer arrived โœจ If the peer becomes unreachable, the value is set to zero ๐Ÿ™ˆ

Transmit Timestamp (peer.xmt, pkt.xmt): this is the local time, in timestamp format, at which the NTP message departed the sender ๐Ÿ’…

System Variables

Table 1<$&tab1> shows the complete set of system variables. On top of the common variables from before, these ones are used by the operating system to synchronize the local clock ๐ŸŽ€

Local Clock (sys.clock): this is the current local time, in timestamp format ๐Ÿ•ฐ๏ธ Local time is derived from the hardware clock of the particular machine and increments at intervals depending on the design used. An appropriate design, including slewing and skew-Compensation mechanisms, is described in Section 5 ๐Ÿ”ฅ

Clock Source (sys.peer): this is a selector identifying the current synchronization source ๐Ÿ‘‘ Usually it's a pointer to a structure containing the peer variables. The special value NULL means there is no currently valid synchronization source ๐Ÿ˜ญ

Peer Variables

Table 2 shows the complete set of peer variables ๐Ÿงš On top of the common variables, these are used by the peer management and measurement functions ๐Ÿ’œ

Configured Bit (peer.config): this is a bit saying the association was created from configuration information, so it should not be demobilized if the peer becomes unreachable ๐ŸŒˆ

Update Timestamp (peer.update): this is the local time, in timestamp format, when the most recent NTP message was received. It's used when calculating the skew dispersion ๐Ÿ“

Reachability Register (peer.reach): this is a shift register of NTP.WINDOW bits used to figure out the reachability status of the peer, with bits entering from the least significant (rightmost) end. A peer is considered reachable if at least one bit in this register is set to one โœจ

Peer Timer (peer.timer): this is an integer counter used to control the interval between transmitted NTP messages. Once it's set to a nonzero value, it decrements at one-second intervals until it hits zero, and then the transmit procedure is called ๐Ÿฆ‹ Note that this timer runs independently of local-clock updates, which implies that the timekeeping system and interval-timer system architecture must be independent of each other ๐Ÿ‘€Table 2<$&tab2>

Packet Variables

Table 3<$&tab3> shows the complete set of packet variables. On top of the common variables, the following variables are defined ๐Ÿ’–

Version Number (pkt.version): this is an integer showing the version number of the sender ๐ŸŽ€ NTP messages will always be sent with the current version number NTP.VERSION, and will always be accepted if the version number matches NTP.VERSION. Exceptions may be advised on a case-by-case basis when the version number changes. Specific guidelines for interoperating between this version and previous versions of NTP are summarized in Appendix D ๐Ÿ“š

Clock-Filter Variables

When the filter and selection algorithms suggested in Section 4 are used, the following state variables are defined on top of the ones from before ๐ŸŒธ

Filter Register (peer.filter): this is a shift register of NTP.SHIFT stages, where each stage stores a 3-tuple made of the measured delay, measured offset and calculated dispersion for a single observation. These 3-tuples enter from the most significant (leftmost) right and get shifted towards the least significant (rightmost) end, and eventually get discarded as new observations arrive ๐Ÿ”ฅ

Valid Data Counter (peer.valid): this is an integer counter showing how many valid samples are left in the filter register. It's used to decide the reachability state and when the poll interval should be increased or decreased ๐Ÿ’…

Offset (peer.offset): this is a signed, fixed-point number showing the offset of the peer clock relative to the local clock, in seconds ๐Ÿงš

Delay (peer.delay): this is a signed fixed-point number showing the roundtrip delay of the peer clock relative to the local clock over the network path between them, in seconds. Note that this variable can be positive or negative, depending on clock precision and skew-error accumulation ๐Ÿ™ˆ

Dispersion (peer.dispersion): this is a signed fixed-point number showing the maximum error of the peer clock relative to the local clock over the network path between them, in seconds. Only values greater than zero are possible ๐ŸŒˆ

Authentication Variables

When the authentication mechanism suggested in Appendix C is used, the following state variables are defined on top of the variables from before. These variables are only used if the optional authentication mechanism described in Appendix C is implemented ๐Ÿ”

Authentication Enabled Bit (peer.authenable): this is a bit saying the association is to operate in the authenticated mode ๐Ÿซถ

Authenticated Bit (peer.authentic): this is a bit saying the last message received from the peer was correctly authenticated ๐Ÿ“

Key Identifier (peer.hostkeyid, peer.peerkeyid, pkt.keyid): this is an integer identifying the cryptographic key used to generate the message-authentication code ๐Ÿ—๏ธ

Cryptographic Keys (sys.key): this is a set of 64-bit DES keys. Each key is constructed as in the Berkeley Unix distributions, which means eight octets, where the seven low-order bits of each octet correspond to the DES bits 1-7 and the high-order bit corresponds to the DES odd-parity bit 8 ๐Ÿฆ‹

Crypto-Checksum (pkt.check): this is a crypto-checksum computed by the encryption procedure ๐Ÿ’œ

Parameters

Table 4<$&tab4> shows the parameters assumed for all implementations operating in the Internet system. It's necessary to agree on the values for these parameters in order to avoid unnecessary network overheads and stable peer associations. The following parameters are assumed fixed and applicable to all associations ๐Ÿ‘‘

Version Number (NTP.VERSION): this is the current NTP version number, which is 3 ๐ŸŽ€

NTP Port (NTP.PORT): this is the port number 123, assigned to NTP by the Internet Assigned Numbers Authority โœจ

Maximum Stratum (NTP.MAXSTRATUM): this is the maximum stratum value that can be encoded as a packet variable. It's also interpreted as "<169>infinity<170>" or unreachable by the subnet routing algorithm ๐Ÿ’€

Maximum Clock Age (NTP.MAXAGE): this is the maximum interval a reference clock will be considered valid after its last update, in seconds โฐ

Maximum Skew (NTP.MAXSKEW): this is the maximum offset error due to skew of the local clock over the interval determined by NTP.MAXAGE, in seconds. The ratio <$Ephi~=~roman {NTP.MAXSKEW over NTP.MAXAGE}> is interpreted as the maximum possible skew rate due to all causes ๐Ÿ”ฅ

Maximum Distance (NTP.MAXDISTANCE): when the selection algorithm suggested in Section 4 is used, this is the maximum synchronization distance for peers acceptable for synchronization ๐ŸŒธ

Minimum Poll Interval (NTP.MINPOLL): this is the minimum poll interval allowed by any peer of the Internet system, in seconds to a power of two ๐Ÿงš

Maximum Poll Interval (NTP.MAXPOLL): this is the maximum poll interval allowed by any peer of the Internet system, in seconds to a power of two ๐Ÿงš

Minimum Select Clocks (NTP.MINCLOCK): when the selection algorithm suggested in Section 4 is used, this is the minimum number of peers acceptable for synchronization ๐Ÿ“

Maximum Select Clocks (NTP.MAXCLOCK): when the selection algorithm suggested in Section 4 is used, this is the maximum number of peers considered for selection ๐Ÿ’œ

Minimum Dispersion (NTP.MINDISPERSE): when the filter algorithm suggested in Section 4 is used, this is the minimum dispersion increment for each stratum level, in seconds ๐ŸŒˆ

Maximum Dispersion (NTP.MAXDISPERSE): when the filter algorithm suggested in Section 4 is used, this is the maximum peer dispersion and the dispersion assumed for missing data, in seconds ๐Ÿฆ‹

Reachability Register Size (NTP.WINDOW): this is the size of the reachability register (peer.reach), in bits ๐Ÿซถ

Filter Size (NTP.SHIFT): when the filter algorithm suggested in Section 4 is used, this is the size of the clock filter (peer.filter) shift register, in stages ๐ŸŽ€

Filter Weight (NTP.FILTER): when the filter algorithm suggested in Section 4 is used, this is the weight used to compute the filter dispersion โญ

Select Weight (NTP.SELECT): when the selection algorithm suggested in Section 4 is used, this is the weight used to compute the select dispersion ๐Ÿ’…


Modes of Operation

Except in broadcast mode, an NTP association forms when two peers exchange messages and one or both of them create and maintain an instance of the protocol machine, called an association ๐Ÿงš The association can operate in one of five modes, as shown by the host-mode variable (peer.mode): symmetric active, symmetric passive, client, server and broadcast. Here's what each one means ๐Ÿ‘‘

Symmetric Active (1): a host in this mode sends periodic messages regardless of the reachability state or stratum of its peer. By doing this, the host announces its willingness to synchronize and be synchronized by the peer ๐Ÿ”ฅ

Symmetric Passive (2): this kind of association is usually created when a message arrives from a peer in symmetric active mode, and it only sticks around as long as the peer is reachable and operating at a stratum level less than or equal to the host. Otherwise, the association gets dissolved. However, the association always persists until at least one message has been sent in reply ๐Ÿซถ By operating in this mode, the host announces its willingness to synchronize and be synchronized by the peer ๐Ÿ’–

Client (3): a host in this mode sends periodic messages regardless of the reachability state or stratum of its peer. By operating this way, the host, usually a LAN workstation, announces its willingness to be synchronized by the peer, but not to synchronize it ๐Ÿ“

Server (4): this kind of association is usually created when a client request message arrives, and it exists only to reply to that request, after which it gets dissolved. By operating in this mode, the host, usually a LAN time server, announces its willingness to synchronize, but not to be synchronized by the peer โฐ

Broadcast (5): a host in this mode sends periodic messages regardless of the reachability state or stratum of the peers. By operating this way, the host, usually a LAN time server on a high-speed broadcast medium, announces its willingness to synchronize all of the peers, but not to be synchronized by any of them ๐ŸŒˆ

A host in client mode occasionally sends an NTP message to a host in server mode, maybe right after rebooting and then at periodic intervals after that. The server responds by simply swapping addresses and ports, filling in the required information, and sending the message back to the client. Servers don't need to keep any state between client requests, while clients are free to manage the intervals between sending NTP messages to suit local conditions. In these modes, the protocol machine described in this document can be pretty much simplified to a basic remote-procedure-call mechanism without significant loss of accuracy or robustness, especially over high-speed LANs ๐Ÿฆ‹

In the symmetric modes, the client/server distinction (almost) disappears ๐Ÿช„ Symmetric passive mode is intended for use by time servers operating near the root nodes (lowest stratum) of the synchronization subnet, with a relatively large number of peers on an intermittent

basis. Okay so in this mode the peer's identity doesn't have to be known ahead of time ๐Ÿฆ‹, because the association and its state variables only get created once an NTP message actually shows up ๐Ÿ“ฌ. Bonus: the state storage can be reused when the peer goes unreachable or drops to a higher stratum level, which makes it ineligible as a synchronization source ๐Ÿงนโœจ

Symmetric active mode is for time servers sitting near the end nodes (highest stratum) of the synchronization subnet ๐ŸŒธ. Reliable time service can usually be kept up with two peers at the next lower stratum level and one peer at the same stratum level ๐Ÿ’–. So the rate of ongoing polls usually isn't a big deal, even when connectivity is lost and an error message comes back for every single poll ๐Ÿ’€

Normally, one peer runs in an active mode (symmetric active, client or broadcast) as set up by a startup file, while the other runs in a passive mode (symmetric passive or server), often without any prior configuration ๐ŸŽ€. Both peers can be set to symmetric active though ๐Ÿ‘‘. Now, here's the tea: an error condition happens when both peers are in the same mode, as long as that mode isn't symmetric active ๐Ÿ™ˆ. In that case each peer ignores messages from the other, so any earlier associations get demobilized because of reachability failure ๐Ÿ˜ญ

Two peers in the same mode (that isn't symmetric active) = each one ignores the other = the old associations get dropped for reachability failure. Not me making a mess ๐Ÿ’…

Broadcast mode is for high-speed LANs with lots of workstations, where the highest accuracies aren't required ๐ŸŒˆ. The typical setup is one or more time servers on the LAN sending periodic broadcasts, and then the workstations work out the time based on a preconfigured latency on the order of a few milliseconds โญ. Just like the client/server modes, the protocol machine can be seriously simplified here ๐Ÿงš. Still, a modified form of the clock selection algorithm might be useful if you're running multiple time servers for extra reliability ๐Ÿซถ


Event Processing โœจ

Okay, the big events in NTP are when a peer timer (peer.timer) expires ๐Ÿ•ฐ๏ธ (there's one for each peer with an active association), and when an NTP message arrives from one of the various peers ๐Ÿ“จ. Events can also pop up from an operator command or a detected system fault, like a primary reference source failure ๐Ÿšจ. This section walks through the procedures that run when all of that happens ๐Ÿ’œ

Notation Conventions ๐Ÿ”ค

The NTP filtering and selection algorithms work with a set of variables for clock offset (<$Etheta ,~THETA>), roundtrip delay (<$Edelta ,~DELTA>) and dispersion (<$Eepsilon ,~EPSILON>). When you need to tell them apart, lowercase Greek letters are used for variables relative to a peer ๐Ÿ“, while uppercase Greek letters are used for variables relative to the primary reference source(s), meaning via the peer all the way to the root of the synchronization subnet ๐ŸŒณ. Subscripts identify the particular peer whenever it isn't clear from context ๐Ÿ”. All of this is built on a quantity called the synchronization distance (<$Elambda ,~LAMBDA>), which gets computed from roundtrip delay and dispersion, as described below โœจ

As described in Appendix H, the peer dispersion <$Eepsilon> has a few parts ๐Ÿงฉ. There's the measurement error <$Erho~=~1~<< <<~roman sys.precision>, the skew-error accumulation <$Ephi tau> (where <$Ephi~=~roman {NTP.MAXSKEW over NTP.MAXAGE}> is the maximum skew rate and <$Etau~=~roman {sys.clock~-~peer.update}> is the interval since the last update), and the filter (sample) dispersion <$Eepsilon sub sigma> that the clock-filter algorithm computes ๐Ÿ’…. The root dispersion <$EEPSILON> includes the selected peer dispersion <$Eepsilon>, the skew-error accumulation <$Ephi tau>, and the root dispersion for the peer itself ๐ŸŒŸ. The system dispersion includes the select (sample) dispersion <$Eepsilon sub xi> computed by the clock-select algorithm, plus the absolute initial clock offset <$E| THETA |> handed to the local-clock algorithm ๐Ÿซง. Both <$Eepsilon> and <$EEPSILON> are dynamic, since they depend on the elapsed time <$Etau> since the last update, plus the sample dispersions the algorithms calculate ๐Ÿฆ‹

Every time the relevant peer variables get updated, all the dispersions tied to that peer get updated too, to reflect the skew-error accumulation ๐Ÿ”„. Here's the summary:

<$Etheta~==~roman peer.offset> ,
<$Edelta~==~roman peer.delay> ,
<$Eepsilon~==~roman peer.dispersion~=~rho~+~phi tau~+~epsilon sub sigma>
,
<$Elambda~==~epsilon~+~{| delta |} over 2> ,

Here <$Etau> is the interval since the original timestamp (the one <$Etheta> and <$Edelta> were figured out from) got transmitted, all the way up to now, and <$Eepsilon sub sigma> is the filter dispersion (see the clock-filter procedure below) ๐Ÿ•ฐ๏ธ. The variables relative to the root of the synchronization subnet via peer i work out like this ๐ŸŒฟ:

<$ETHETA sub i~==~theta sub i> ,
<$EDELTA sub i~==~roman peer.rootdelay~+~delta sub i> ,
<$EEPSILON sub i~==~roman peer.rootdispersion~+~epsilon sub i~+~phi tau
sub i> ,
<$ELAMBDA sub i~==~EPSILON sub i~+~{| DELTA sub i |} over 2> ,

Everything here is understood to be about the ith peer ๐Ÿ’œ. Finally, assuming the ith peer is picked for synchronization, the system variables come out like this ๐Ÿ‘‘:

<$ETHETA~=~>combined final offset ,
<$EDELTA~=~DELTA sub i> ,
<$EEPSILON~=~EPSILON sub i~+~epsilon sub xi~+~| THETA |> ,
<$ELAMBDA~=~LAMBDA sub i> ,

Here <$Eepsilon sub xi> is the select dispersion (see the clock-selection procedure below) โœจ

The informal pseudo-code that does all these computations is shown below ๐Ÿง. Heads up, the pseudo-code isn't written in any particular language, although it has lots of similarities to C ๐Ÿ’•. The specific details of the important algorithms are illustrated further in the C-language routines in Appendix I ๐ŸŽ€

Transmit Procedure ๐Ÿ“ค

The transmit procedure runs when the peer timer decrements to zero for all modes except client mode with a broadcast server, and server mode in every case ๐Ÿ™…โ€โ™€๏ธ. In client mode with a broadcast server, messages are never sent ๐Ÿ“ต. In server mode, messages only get sent in response to received messages ๐Ÿ’Œ. This procedure also gets called by the receive procedure when an NTP message arrives that doesn't result in a persistent association ๐Ÿ”

begin transmit procedure

First up, the packet buffer gets initialized and the packet variables get copied ๐Ÿ“ฆ. The value skew is needed to account for the skew-error that piled up over the interval since the local clock was last set โฐ

        <$Eroman pkt.peeraddr~<<-~roman peer.hostaddr>;         /* copy
system and peer variables */
        <$Eroman pkt.peerport~<<-~roman peer.hostport>;
        <$Eroman pkt.hostaddr~<<-~roman peer.peeraddr>;
        <$Eroman pkt.hostport~<<-~roman peer.peerport>;
        <$Eroman pkt.leap~<<-~roman sys.leap>;
        <$Eroman pkt.version~<<-~roman NTP.VERSION>;
        <$Eroman pkt.mode~<<-~roman peer.mode>;
        <$Eroman pkt.stratum~<<-~roman sys.stratum>;
        <$Eroman pkt.poll~<<-~roman peer.hostpoll>;
        <$Eroman pkt.precision~<<-~roman sys.precision>;
        <$Eroman pkt.rootdelay~<<-~roman sys.rootdelay>;
        if (sys.leap = 112 or (sys.clock <196> sys.reftime) >>
NTP.MAXAGE)
                <$Eskew~<<-~roman NTP.MAXSKEW>;
        else
                <$Eskew~<<-~phi roman {(sys.clock~-~sys.reftime)}>;
        <$Eroman {pkt.rootdispersion~<<-~roman
sys.rootdispersion~+~(1~<< <<~sys.precision)}~+~skew>;
        <$Eroman pkt.refid~<<-~roman sys.refid>;
        <$Eroman pkt.reftime~<<-~roman sys.reftime>;

Now, the transmit timestamp pkt.xmt is used later to validate the reply, so implementations must save the exact value transmitted ๐Ÿ”. Also, the order of copying the timestamps should be designed so that the time spent formatting and copying the data doesn't degrade accuracy โšก

        <$Eroman pkt.org~<<-~roman peer.org>;
/* copy timestamps */
        <$Eroman pkt.rec~<<-~roman peer.rec>;
        <$Eroman pkt.xmt~<<-~roman sys.clock>;
        <$Eroman peer.xmt~<<-~roman pkt.xmt>;

The call to encrypt only exists if authentication is implemented ๐Ÿ”‘. If authentication is enabled, the delay to encrypt the authenticator might degrade accuracy ๐Ÿ˜ฌ. So implementations should include a system state variable (one that isn't mentioned anywhere else in this specification) holding an offset calculated to match the expected encryption delay, which corrects the transmit timestamp taken from the local clock ๐Ÿงฎ

        #ifdef (authentication implemented)     /* see Appendix C */
                call encrypt;
                #endef
        send packet;

The reachability register gets shifted one position to the left, with a zero filling the vacated bit ๐ŸชŸ. If all the bits of this register are zero, the clear procedure gets called to purge the clock filter and reselect the synchronization source, if necessary ๐Ÿงน. And if the association wasn't configured by the initialization procedure, the association gets demobilized ๐Ÿ‘‹

        <$Eroman peer.reach~<<-~roman peer.reach~<< <<~1>;
/* update reachability */
        if (<$Eroman peer.reach~=~0> and <$Eroman peer.config~=~0>)
begin
                demobilize association;
                exit;
                endif

If valid data got shifted into the filter register at least once during the preceding two poll intervals (the low-order bit of peer.reach set to one), the valid data counter goes up ๐Ÿ“ˆ. After eight such valid intervals, the poll interval goes up too ๐ŸŒˆ. Otherwise, the valid data counter and poll interval both go down, and the clock-filter procedure gets called with zero values for offset and delay and NTP.MAXDISPERSE for dispersion ๐Ÿ“‰. Then the clock-select procedure gets called to reselect the synchronization source, if necessary ๐Ÿ”„

        if (<$Eroman peer.reach~&~6~!=~0>)                      /* test
two low-order bits (shifted) */
                if (<$Eroman peer.valid~<<~roman NTP.SHIFT>)    /* valid
data received */
                        <$Eroman peer.valid~<<-~roman peer.valid~+~1>;
                        else <$Eroman peer.hostpoll~<<-~roman
peer.hostpoll~+~1>;
        else begin
                <$Eroman peer.valid~<<-~roman peer.valid~-~1>;  /*
nothing heard */
                <$Eroman peer.hostpoll~<<-~roman peer.hostpoll~-~1>);
                call clock-filter(0, 0, NTP.MAXDISPERSE);
                call clock-select;                      /* select clock
source */
                endif
        call poll-update;
        end transmit procedure;

Receive Procedure ๐Ÿ“ฅ

The receive procedure runs whenever an NTP message arrives ๐Ÿ’Œ. It validates the message, interprets the various modes, and calls other procedures to filter the data and select the synchronization source ๐Ÿง. If the version number in the packet doesn't match the current version, the message may be discarded ๐Ÿ—‘๏ธ. Exceptions may be advised on a case-by-case basis at times when the version is changed ๐Ÿคทโ€โ™€๏ธ. If the NTP control messages described in Appendix B are implemented and the packet mode is 6 (control), the control-message procedure gets called ๐ŸŽ›๏ธ. The source and destination Internet addresses and ports in the IP and UDP headers get matched to the correct peer ๐ŸŽฏ. If there's no match, a new instantiation of the protocol machine is created and the association gets mobilized ๐Ÿฆ‹

begin receive procedure
        if (<$Eroman pkt.version~!=~roman NTP.VERSION>) exit;
        #ifdef (control messages implemented)
                if (<$Eroman pkt.mode~=~6>) call control-message;
                #endef
        for (all associations)                  /* access control goes
here */
                match addresses and ports to associations;
        if (no matching association)
                call receive-instantiation procedure;   /* create
association */

The call to decrypt only exists if authentication is implemented ๐Ÿ”“

        #ifdef (authentication implemented)     /* see Appendix C */
                call decrypt;
                #endef

If the packet mode is nonzero, that value becomes mode for the next step. If it's zero, the peer is an old NTP version, so we work out the mode from the port numbers instead, as described in Section 3.3. โœจ๐ŸŽ€

        if (pkt.mode = 0)                               /* for
compatibility with old versions */
                mode <- (see Section 3.3);
        else
                mode <- pkt.mode;

Table 5 shows the case labels you get for each combination of peer.mode and mode. ๐Ÿ’…

        case (mode, peer.hostmode)              /* see Table 5 */

error ๐Ÿ’€ If there's an error, the packet just gets ignored. The association gets demobilized, but only if it wasn't configured beforehand. ๐Ÿ™ˆ

error:          if (peer.config = 0) demobilize association;
/* see no evil */
                break;

recv ๐Ÿซถ The packet gets processed, and the association is marked reachable if tests five through eight (the valid header ones from the packet procedure) pass. If tests one through four (valid data) also pass, the clock-update procedure runs and updates the local clock. If the header isn't valid and the association wasn't configured beforehand, it gets demobilized. ๐ŸŒธ

recv:           call packet;                            /* process
packet */
                if (valid header) begin         /* if valid header,
update local clock */
                        peer.reach <- peer.reach | 1;
                        if (valid data) call clock-update;
                        endif
                else
                        if (peer.config = 0) demobilize
association;
                break;

xmit ๐Ÿงš The packet gets processed and an immediate reply goes out. After that, the association is demobilized, unless it was configured beforehand. ๐Ÿ“

xmit:           call packet;                            /* process
packet */
                peer.hostpoll <- peer.peerpoll;
/* send immediate reply */
                call poll-update;
                call transmit;
                if (peer.config = 0) demobilize association;
                break;

pkt ๐Ÿฆ‹ The packet gets processed, and the association is marked reachable if tests five through eight (valid header) pass. If tests one through four (valid data) pass too, the clock-update procedure runs. If the header isn't valid, though, and the association wasn't configured beforehand, we send an immediate reply and demobilize it. ๐Ÿ‘‘

pkt:            call packet;                            /* process
packet */
                if (valid header) begin         /* if valid header,
update local clock */
                        peer.reach <- peer.reach | 1;
                        if (valid data) call clock-update;
                        endif
                else if (peer.config = 0) begin
                        peer.hostpoll <- peer.peerpoll; /* send
immediate reply */
                        call poll-update;
                        call transmit;
                        demobilize association;
                        endif
                endcase
        end receive procedure;

Packet Procedure โœจ

The packet procedure is where the magic happens, bestie. ๐Ÿ”ฅ It checks that the message is valid, works out the delay and offset samples, and then hands everything off to the other procedures that filter the data and pick the synchronization source. ๐Ÿง 

Test 1 checks that the transmit timestamp doesn't match the last one we got from the same peer. If it does match, the message might just be an old duplicate, so it's a no-go. ๐Ÿ™…โ€โ™€๏ธ Test 2 checks that the originate timestamp matches the last one we sent to that same peer. If it doesn't, the message might be out of order, bogus, or worse. ๐Ÿ˜ญ In broadcast mode (5), the apparent roundtrip delay comes out as zero, so the full accuracy of the time-transfer operation may not be achievable. Still, the accuracy might be good enough for most purposes. ๐ŸŒˆ The poll-update procedure gets called with peer.hostpoll as its argument, because peer.peerpoll may have changed by then. ๐Ÿ’–

begin packet procedure
        peer.rec <- sys.clock;                 /*
capture receive timestamp */
        if (pkt.mode != 5) begin
                test1 <- ( pkt.xmt != peer.org);   /* test
1 */
                test2 <- ( pkt.org = peer.xmt);    /* test
2 */
                endif
        else begin
                pkt.org <- peer.rec;
/* fudge missing timestamps */
                pkt.rec <- pkt.xmt;
                test1 <- true;
/* fake tests */
                test2 <- true;
                endif
        peer.org <- pkt.xmt;
/* update originate timestamp */
        peer.peerpoll <- pkt.poll;
/* adjust poll interval */
        call poll-update(peer.hostpoll);

Test 3 needs both the originate and receive timestamps to be nonzero. ๐Ÿซง If either one is zero, it means the association hasn't synchronized yet, or it's lost reachability in one or both directions. ๐Ÿฅบ

        test3 <- ( pkt.org != 0 and pkt.rec != 0); /* test 3 */

Here's how we calculate the roundtrip delay and the clock offset relative to the peer. ๐Ÿงฎ Number the times of sending and receiving NTP messages as shown in Figure 2, and let i be an even integer. Then T(i-3), T(i-2), T(i-1) and T(i) are the contents of the pkt.org, pkt.rec, pkt.xmt and peer.rec variables, respectively. The clock offset theta, roundtrip delay delta, and dispersion epsilon of the host relative to the peer work out like this:

delta   = (T_i - T_{i-3}) - (T_{i-1} - T_{i-2})

theta   = ((T_{i-2} - T_{i-3}) + (T_{i-1} - T_i)) / 2

epsilon = (1 << sys.precision) + phi * (T_i - T_{i-3})

Where, as before, phi = NTP.MAXSKEW / NTP.MAXAGE. โœจ Epsilon is the maximum error or dispersion from measurement error at the host, plus the local-clock skew that builds up over the interval since the last message went out to the peer. ๐Ÿ“ˆ After that, the clock-filter procedure updates the dispersion. ๐Ÿ’…

Here's the vibe: this is a continuously sampled, returnable-time system, and it's used in some digital telephone networks [BEL86]. ๐Ÿ“ž The big perks are that the order and timing of the messages don't matter, and reliable delivery isn't needed. ๐ŸŒŸ Obviously, how accurate things get depends on the statistical properties of the outbound and inbound data paths. If you want more analysis and experimental results on this, check out [MIL90] and Appendix H. ๐Ÿ“š Test 4 requires that the calculated delay stays within reasonable bounds. ๐ŸŽ€

        test4 <- (|delta| <= NTP.MAXDISPERSE and epsilon <= NTP.MAXDISPERSE);  /* test 4 */

Test 5 only happens if the authentication mechanism from Appendix C is implemented. ๐Ÿ” It needs either that authentication is explicitly disabled, or that the authenticator is present and correct according to the decrypt procedure. ๐Ÿซถ

        #ifdef (authentication implemented)     /* test 5 */
                test5 <- ((peer.config = 1 and peer.authenable = 0)
or peer.authentic = 1);
                #endef

Test 6 needs the peer clock to be synchronized, and the interval since the peer clock was last updated has to be positive and less than NTP.MAXAGE. โฐ Test 7 makes sure the host won't synchronize with a peer that has a greater stratum. ๐Ÿชœ Test 8 requires that the header has reasonable values for the pkt.rootdelay and pkt.rootdispersion fields. ๐ŸŒธ

        test6 <- ( pkt.leap != 11_2 and          /* test
6 */
                pkt.reftime <= pkt.xmt < pkt.reftime + NTP.MAXAGE);
        test7 <- (pkt.stratum <= sys.stratum) and  /* test
7 */
                 (pkt.stratum < NTP.MAXSTRATUM);
        test8 <- (|pkt.rootdelay| <= NTP.MAXDISPERSE
and     /* test 8 */
                pkt.rootdispersion <= NTP.MAXDISPERSE);

When it comes to what gets processed further, the packet has valid (synchronized) data if tests one through four pass, which is (test1 & test2 & test3 & test4 = 1), no matter what the other tests say. ๐Ÿ“ Only packets with valid data can be used to calculate offset, delay and dispersion values. The packet has a valid header if tests five through eight pass, which is (test5 & test6 & test7 & test8 = 1), again no matter what else happens. ๐Ÿงš Only packets with valid headers can be used to decide whether a peer can be selected for synchronization. Heads up: test 1 and test 2 aren't used in broadcast mode, so they're forced to true, since the originate and receive timestamps are undefined there. ๐Ÿ™ˆ

The clock-filter procedure gets called to produce the delay (peer.delay), offset (peer.offset) and dispersion (peer.dispersion) for the peer. ๐Ÿซง The clock-filter algorithm isn't an official, required part of the NTP specification, since other algorithms might work well in practice too. ๐ŸŒˆ That said, one that's known to work well in the Internet environment is described in Section 4, and its use is recommended. ๐Ÿ‘‘

        if (not valid header) exit;
        peer.leap <- pkt.leap;                 /* copy
packet variables */
        peer.stratum <- pkt.stratum;
        peer.precision <- pkt.precision;
        peer.rootdelay <- pkt.rootdelay;
        peer.rootdispersion <- pkt.rootdispersion;
        peer.refid <- pkt.refid;
        peer.reftime <- pkt.reftime;
        if (valid data) call clock-filter(theta, delta, epsilon);
/* process sample */
        end packet procedure;

Clock-Update Procedure ๐Ÿฆ‹

The clock-update procedure gets called from the receive procedure once the clock-filter procedure has worked out valid clock offset, delay and dispersion data for the current peer. ๐Ÿ’– The clock-selection and clock-combining procedures produce the final clock correction THETA, which the local-clock procedure uses to update the local clock. If no candidates survive those procedures, the clock-update procedure just exits without doing anything else. ๐Ÿ˜Œ

begin clock-update procedure
        call clock-select;                              /* select clock
source */
        if (sys.peer != peer) exit;

Sometimes the local clock gets reset instead of slewed to its final value. ๐Ÿ”„ When that happens, the clear procedure gets called for every peer, which purges the clock filter, resets the poll interval, and reselects the synchronization source if needed. โœจ Also, the local-clock procedure sets the leap bits sys.leap to unsynchronized, which is 11 in binary (11_2) in this case. ๐Ÿ™…โ€โ™€๏ธ That way, no other peer will try to sync to the host until the host picks a peer for synchronization again. ๐Ÿซถ

The distance procedure works out the root delay DELTA, the root dispersion EPSILON, and the root synchronization distance LAMBDA, all measured through the peer to the root of the synchronization subnet. ๐ŸŒณ The host won't synchronize to the selected peer if the distance is greater than NTP.MAXDISTANCE. ๐Ÿšซ The minimum clamp at NTP.MINDISPERSE exists to discourage subnet route flaps, which can happen with Bellman-Ford algorithms when roundtrip delays are small. ๐ŸŒŠ

        LAMBDA <- distance (peer);             /* update system
variables */
        if (LAMBDA >= NTP.MAXDISTANCE) exit;
        sys.leap <- peer.leap;
        sys.stratum <- peer.stratum + 1;
        sys.refid <- peer.peeraddr;
        call local-clock;
        if (local clock reset) begin                    /* if reset,
clear state variables */
                sys.leap <- 11_2;
                for (all peers) call clear;
                endif
        else begin
                sys.peer <- peer;                    /* if
not, adjust local clock */
        <$Eroman sys.rootdelay~<<-~DELTA>;
        <$Eroman sys.rootdispersion~<<-~EPSILON~+~max ( epsilon
sub xi~+~| THETA |,~roman NTP.MINDISPERSE)>;
        endif
        <$Eroman sys.reftime~<<-~roman sys.clock>;
end clock-update procedure;

Okay so bestie, this is where things get really fun ๐ŸŽ€ Some systems have a super precise timing source that gives you a train of pulses, one every second โฑ๏ธโœจ That's usually paired with a timecode source, like a radio clock or even NTP itself, which tells you which second, minute, hour and day it is ๐Ÿ“… In those setups, the system variables get pointed at whatever the pulses come from ๐Ÿซถ For configs with a primary reference source, like a radio clock or a calibrated atomic clock, the stratum is set to one ๐Ÿ‘‘ as long as that's the actual sync source, whether or not the primary-clock procedure is being used. Iconic behaviour, honestly ๐Ÿ’…

Here's the tea ๐Ÿต The clock-selection and local-clock algorithms are not an integral part of the NTP spec, because other algorithms might give equivalent performance ๐Ÿฆ‹ Still, the clock-selection algorithm in Section 4 is one that works really well on the Internet, and a local-clock algorithm is described in Section 5. Their use is recommended ๐Ÿ’œ The Section 4 selection usually picks the peer with the lowest stratum and minimum synchronization distance out of everyone available, unless that peer looks like a falseticker ๐Ÿ™ˆ The result is that all these algorithms team up to build a minimum-weight spanning tree relative to the primary reference time servers, which gives you a hierarchical-master-slave synchronization subnet ๐ŸŒˆ Literally a whole family tree of clocks ๐Ÿงš

Primary-Clock Procedure โฐ

When a primary reference source, like a radio clock, is hooked up to the host, it's easiest to fold its info into the database as if it were just an ordinary peer ๐ŸŽ€ In the primary-clock procedure, the clock gets polled about once a minute, and the timecode that comes back is used to make a new update for the local clock โœจ When peer.timer hits zero for a primary clock peer, the transmit procedure is not called. Instead, the radio clock gets polled, usually with an ASCII string made for that exact purpose ๐Ÿซถ When a valid timecode comes back, it's converted into NTP timestamp format and the peer variables get updated ๐Ÿ’– The value of peer.leap comes from the leap-warning bit in the timecode if it's available, or the operator sets it by hand ๐Ÿ”ฅ The value for peer.peeraddr becomes sys.refid when the clock-update procedure runs, and it's just an ASCII string describing the clock type (see Appendix A) ๐ŸŒธ

begin primary-clock-update procedure
        <$Eroman peer.leap~<<-~"from"~radio~or~operator>;       /* copy
variables */
        <$Eroman peer.peeraddr~<<-~ASCII~identifier>;
        <$Eroman peer.rec~<<-~radio~timestamp>;
        <$Eroman peer.reach~<<-~1>;
        call clock-filter(<$Eroman {sys.clock~-~peer.rec,~0,~1~<<
<<~peer.precision}>);   /* process sample */
        call clock-update;                              /* update local
clock */
        end primary-clock-update procedure;

Initialization Procedures ๐ŸŒฑ

The initialization procedures are the setup crew ๐Ÿงš They get the system, its peers and its associations all set up and ready to go ๐Ÿ’…

Initialization Procedure ๐Ÿš€

This one runs on reboot or restart of the NTP daemon ๐Ÿ”„ The local clock is basically undefined at reboot, however, some equipment can get an estimate from the reboot environment, like a battery-backed clock/calendar ๐Ÿ”‹ The precision variable comes from the intrinsic architecture of the local hardware clock ๐ŸŽ€ The authentication variables only matter if the authentication mechanism in Appendix C is implemented, and their values come from procedures that are totally outside NTP itself ๐Ÿ”

begin initialization procedure
        #ifdef (authentication implemented)     / * see Appendix C */
                <$Eroman sys.keys~<<-~as~required>;
                #endef;
        <$Eroman sys.leap~<<-~11 sub 2>;
/* copy variables */
        <$Eroman sys.stratum~<<-~0~(undefined)>;
        <$Eroman sys.precision~<<-~host~precision>;
        <$Eroman sys.rootdelay~<<-~0~(undefined)>;
        <$Eroman sys.rootdispersion~<<-~0~(undefined)>;
        <$Eroman sys.refid~<<-~0~(undefined)>;
        <$Eroman sys.reftime~<<-~0~(undefined)>;
        <$Eroman sys.clock~<<-~external~reference>;
        <$Eroman sys.peer~<<-~roman NULL>;
        <$Eroman sys.poll~<<-~roman NTP.MINPOLL>;
        for (all configured peers)                      /* create
configured associations */
                call initialization-instantiation procedure;
        end initialization procedure;

Initialization-Instantiation Procedure ๐Ÿงฉ

This bit is implementation-specific, and the initialization procedure calls it to define an association ๐Ÿ’– The peer addresses and modes come from info read during the reboot procedure or from operator commands ๐Ÿซถ Same deal with the authentication variables: they only matter if Appendix C is implemented, and their values come from procedures outside NTP ๐Ÿ” With the authentication bits set as suggested, only properly authenticated peers can become the synchronization source ๐Ÿ›ก๏ธ

begin initialization-instantiation procedure
        <$Eroman peer.config~<<-~1>;
        #ifdef (authentication implemented)     /* see Appendix C */
                <$Eroman peer.authenable~<<-~1~(suggested)>;
                <$Eroman peer.authentic~<<-~0>;
                <$Eroman peer.hostkeyid~<<-~as~required>;
                <$Eroman peer.peerkeyid~<<-~0>;
                #endef;
        <$Eroman peer.peeraddr~<<-~peer~IP~address>;    /* copy
variables */
        <$Eroman peer.peerport~<<-~roman NTP.PORT>;
        <$Eroman peer.hostaddr~<<-~host~IP~address>;
        <$Eroman peer.hostport~<<-~roman NTP.PORT>;
        <$Eroman peer.mode~<<-~host~mode>;
        <$Eroman peer.peerpoll~<<-~0~(undefined)>;
        <$Eroman peer.timer~<<-~0>;
        <$Eroman peer.delay~<<-~0~(undefined)>;
        <$Eroman peer.offset~<<-~0~(undefined)>;
        call clear;                                     /* initialize
association */
        end initialization-instantiation procedure;

Receive-Instantiation Procedure ๐Ÿ“จ

This one gets called from the receive procedure whenever a brand-new peer shows up ๐Ÿ‘€ It sets up the peer variables and mobilizes the association ๐ŸŽ€ If the message comes from a peer in client mode (3), the host mode becomes server mode (4) ๐Ÿ“ Otherwise, it becomes symmetric passive mode (2) ๐Ÿฆ‹ The authentication variables only matter if Appendix C is implemented ๐Ÿ” If it is, only properly authenticated non-configured peers can become the synchronization source ๐Ÿ’…

begin receive-instantiation procedure
        #ifdef (authentication implemented)     /* see Appendix C */
                <$Eroman peer.authenable~<<-~0>;
                <$Eroman peer.authentic~<<-~0>;
                <$Eroman peer.hostkeyid~<<-~as~required>;
                <$Eroman peer.peerkeyid~<<-~0>;
                #endef
        <$Eroman peer.config~<<-~0>;                            /* copy
variables */
        <$Eroman peer.peeraddr~<<-~roman pkt.peeraddr>;
        <$Eroman peer.peerport~<<-~roman pkt.peerport>;
        <$Eroman peer.hostaddr~<<-~roman pkt.hostaddr>;
        <$Eroman peer.hostport~<<-~roman pkt.hostport>;
        if (pkt.mode = 3)                               /* determine
mode */
                <$Eroman peer.mode~<<-~4>;
                else
                <$Eroman peer.mode~<<-~2>;
        <$Eroman peer.peerpoll~<<-~0~(undefined)>;
        <$Eroman peer.timer~<<-~0>;
        <$Eroman peer.delay~<<-~0~(undefined)>;
        <$Eroman peer.offset~<<-~0~(undefined)>;
        call clear;                                     /* initialize
association */
        end receive-instantiation procedure;

Primary Clock-Instantiation Procedure โญ

This procedure is called from the initialization procedure to set up the state variables for the primary clock ๐ŸŒŸ The value for peer.precision comes from the radio clock spec and hardware interface ๐Ÿ”ง The value for peer.rootdispersion is nominally ten times the inherent maximum error of the radio clock ๐Ÿ’œ For example, that's <$E10~mu s> for a calibrated atomic clock, 10 ms for a WWVB or GOES radio clock, and 100 ms for a less accurate WWV radio clock ๐Ÿงš

begin clock-instantiation procedure
        <$Eroman peer.config~<<-~1>;                            /* copy
variables */
        <$Eroman peer.peeraddr~<<-~0~undefined>;
        <$Eroman peer.peerport~<<-~0~(not~used)>;
        <$Eroman peer.hostaddr~<<-~0~(not~used)>;
        <$Eroman peer.hostport~<<-~0~(not~used)>;
        <$Eroman peer.leap~<<-~11 sub 2>;
        <$Eroman peer.mode~<<-~0~(not~used)>;
        <$Eroman peer.stratum~<<-~0>;
        <$Eroman peer.peerpoll~<<-~0~(undefined)>;
        <$Eroman peer.precision~<<-~clock~precision>;
        <$Eroman peer.rootdelay~<<-~0>;
        <$Eroman peer.rootdispersion~<<-~clock~dispersion>;
        <$Eroman peer.refid~<<-~0~(not~used)>;
        <$Eroman peer.reftime~<<-~0~(undefined)>;
        <$Eroman peer.timer~<<-~0>;
        <$Eroman peer.delay~<<-~0~(undefined)>;
        <$Eroman peer.offset~<<-~0~(undefined)>;
        call clear;                                     /* initialize
association */
        end clock-instantiation procedure;

In some setups with a calibrated atomic clock or LORAN-C receiver, the primary reference source might only give you a seconds pulse ๐Ÿซง It doesn't come with a full timecode, so you can't figure out the seconds numbering from it alone ๐Ÿ”ข In that case, the numbering can come from other sources, like a radio clock or even other NTP peers ๐ŸŒˆ The primary clock variables should still reflect the primary reference source, not the seconds-numbering source ๐Ÿ’– But if the seconds-numbering source fails or is known to be running incorrectly, updates from the primary reference source should be suppressed as if it had failed ๐Ÿšซ

Clear Procedure ๐Ÿงน

The clear procedure gets called when something happens that causes a big change in reachability state or could seriously disrupt the local clock ๐Ÿ˜ฑ

begin clear procedure
        <$Eroman peer.org~<<-~0~(undefined)>;                   /* mark
timestamps undefined */
        <$Eroman peer.rec~<<-~0~(undefined)>;
        <$Eroman peer.xmt~<<-~0~(undefined)>;
        <$Eroman peer.reach~<<-~0>;                             /* reset
state variables */
        <$Eroman peer.filter~<<-~[0,~,0,~roman NTP.MAXDISPERSE]>;   /*
all stages */
        <$Eroman peer.valid~<<-~0>;
        <$Eroman peer.dispersion~<<-~roman NTP.MAXDISPERSE>;
        <$Eroman {peer.hostpoll~<<-~NTP.MINPOLL}>;              /* reset
poll interval */
        call poll-update;
        call clock-select;                              /* select clock
source */
        end clear procedure;

Poll-Update Procedure ๐Ÿ”

The poll-update procedure gets called when a significant event might change the poll interval or the peer timer ๐Ÿ’ซ It checks the host poll interval (peer.hostpoll) and the peer poll interval (peer.peerpoll), and clamps each one within the valid range ๐ŸŽฏ If the peer is selected for synchronization, the value gets clamped even further, as a function of the computed compliance (see Section 5) ๐Ÿฆ‹

begin poll-update procedure
        <$Etemp~<<-~roman peer.hostpoll>;                       /*

okay so we're still in the poll-update procedure, and first up is figuring out the host poll interval ๐ŸŽ€

determine host poll interval */
        if (<$Epeer~=~roman sys.peer>)
                <$Etemp~<<-~min (temp,~roman {sys.poll,~NTP.MAXPOLL)}>;
        else
                <$Etemp~<<-~min (temp,~roman NTP.MAXPOLL)>;
        <$Eroman peer.hostpoll~<<-~max (temp,~roman NTP.MINPOLL)>;
        <$Etemp~<<-~1~<< << ~min ( roman {peer.hostpoll,~max (peer.peerpoll,~NTP.MINPOLL)})>;

ok so if the poll interval is unchanged and the peer timer is zero, the timer just gets reset ๐Ÿ”„ if the poll interval changed and the new timer value is bigger than the present one, no extra action needed โœจ otherwise the peer timer must be reduced. but when it gets reduced it's super important to discourage the peers from syncing up their transmissions, bestie ๐Ÿ™ˆ a prudent precaution is to randomize the first transmission after the timer is reduced, for instance by the sneaky technique illustrated.

        if (peer.timer = 0)                             /* reset peer timer */
                <$Eroman peer.timer~<<-~temp>;
        else if (<$Eroman peer.timer~>>~temp>)
                <$Eroman peer.timer~<<-~( roman sys.clock~&~(temp~-~1))~+~1>;
        end poll-update procedure;

Synchronization Distance Procedure

the distance procedure is the one that works out the synchronization distance from the peer variables for the peer peer ๐Ÿ’–

begin distance(peer) procedure;
        <$EDELTA~<<-~roman {peer.rootdelay~+~|peer.delay|}>;
        <$EEPSILON~<<-~roman {peer.rootdispersion~+~peer.dispersion~+~phi (sys.clock~-~peer.update)}>;
        <$ELAMBDA~<<-~EPSILON~+~{| DELTA |} over 2> ;
        end distance procedure;

okay so here's the tea ๐Ÿ“ <$EDELTA> may be negative in some cases, but <$EEPSILON> and <$ELAMBDA> are always positive. no cap, that's a big deal ๐Ÿ˜ญ

Access Control Issues

the NTP design is giving redundancy ๐Ÿ’… accidental or malicious data modification (tampering) or destruction (jamming) at a time server should not, in general, result in timekeeping errors somewhere else in the synchronization subnet. but this only works if you have redundant time servers and diverse network paths, plus the assumption that tampering or jamming won't happen at lots of time servers across the synchronization subnet at the same time ๐Ÿซถ in principle, you can engineer the subnet's vulnerability by picking time servers that are known to be trusted and only letting those become the synchronization source. the authentication procedures in Appendix C are one mechanism to enforce this. the catch is that the encryption algorithms can be quite CPU-intensive and can seriously degrade accuracy, unless you take precautions like the ones mentioned in the description of the transmit procedure ๐Ÿ’€

while it's not a required feature of NTP itself, some implementations may include an access-control feature that stops unauthorized access and controls which peers are allowed to update the local clock ๐Ÿ”’ for this it's useful to split access into three categories: accesses that are preauthorized as trusted, accesses that are preauthorized as friendly, and all other (non-preauthorized) accesses ๐Ÿ‘‘ presumably preauthorization happens through entries in the configuration file or some kind of ticket-management system like Kerberos [STE88]. in this model, only trusted accesses can make the peer become the synchronization source. friendly accesses can't make the peer the synchronization source, but NTP messages and timestamps still get returned as specified ๐Ÿฆ‹

it doesn't seem useful to keep a secret clock, which is what you'd get by restricting non-preauthorized accesses, unless the goal is to hide the time server's existence entirely ๐Ÿ™ˆ well-behaved Internet hosts are expected to return an ICMP service-unavailable error message if a service isn't implemented or resources aren't available. but for NTP the resources needed are minimal, so there's little need to restrict requests that are only meant to read the clock โœจ a simple but effective access-control mechanism is to treat all associations preconfigured in a symmetric mode or client mode (modes 1, 2 and 3) as trusted, and all other associations, preconfigured or not, as friendly ๐Ÿงš

if you need a more comprehensive trust model, the design can be based on an access-control list where each entry has a 32-bit Internet address, a 32-bit mask and a three-bit mode ๐Ÿ’œ if the logical AND of the source address (pkt.peeraddr) and the mask in an entry matches the corresponding address in the entry, and the mode (pkt.mode) matches the mode in the entry, the access is allowed. otherwise an ICMP error message is returned to the requestor. with the right mask, you can restrict requests by mode to individual addresses, a particular subnet or net addresses, or have no restriction at all ๐ŸŒˆ the access-control list would then act as a filter that controls which peers can create associations.

Filtering and Selection Algorithms

okay this section is so important bestie ๐Ÿ”ฅ accuracy and reliability of time distribution depend heavily on the complex of algorithms that reduce the effect of statistical errors and falsetickers caused by failures in various subnet components, reference sources or propagation media. the algorithms suggested here were developed and refined over several years of operation on the Internet, under widely varying topologies, speeds and traffic regimes ๐Ÿ’… they're believed to be the best available right now, but they aren't an integral part of the NTP specification, since other algorithms with similar or better performance might be devised in the future โœจ

however, not every time server or client in an NTP synchronization subnet has to implement these algorithms ๐ŸŽ€ simple workstations, for example, may skip one or both of them for simplicity if their accuracy and reliability requirements allow it. still, an NTP server providing synchronization to a sizable community, like a university campus or research lab, would be expected to implement these algorithms or others proven to have equivalent functionality ๐Ÿ’– a full discussion of the design principles and performance is in [MIL91a].

for the NTP filter and selection algorithms to work well, it's useful to have a measure of recent sample variance recorded for each peer ๐Ÿฆ‹ the measure they picked is based on first-order differences, which are easy to compute and effective for what they're needed for. there are two measures: the filter dispersion <$Eepsilon sub sigma> and the select dispersion <$Eepsilon sub xi>. both are computed as the weighted sum of the clock offsets in a temporary list sorted by synchronization distance. if <$Etheta sub i ~(0~<<=~i~<<~n)> is the offset of the ith entry, then the sample difference <$Eepsilon sub ij~<~>=~| theta sub i~-~theta sub j |> of the ith entry relative to the jth entry is defined as well. the dispersion relative to the jth entry is defined as <$Eepsilon sub j> and computed as the weighted sum:

<$Eepsilon sub j~=~sum from {i~=~0} to {n~-~1}~epsilon sub ij~w~sup {i+1}>

where w is a weighting factor chosen to control how much synchronization distance influences the dispersion budget ๐Ÿง in the NTP algorithms, w is chosen less than <$E1 / 2>: <$Ew~=~roman NTP.FILTER> for filter dispersion and <$Ew~=~roman NTP.SELECT> for select dispersion. the (absolute) dispersion <$Eepsilon sub sigma> and <$Eepsilon sub xi> as used in the NTP algorithms are defined relative to the 0th entry, <$Eepsilon sub 0>.

there are two procedures described next: the clock-filter procedure, which picks the best offset samples from a given clock, and the clock-selection procedure, which picks the best clock among a hierarchical set of clocks ๐Ÿ‘‘

Clock-Filter Procedure

the clock-filter procedure runs whenever an NTP message or other event produces new data samples ๐Ÿ”„ it takes arguments of the form (<$Etheta ,~delta ,~epsilon>), where <$Etheta> is a sample clock offset measurement and <$Edelta> and <$Eepsilon> are the associated roundtrip delay and dispersion. it works out the filtered clock offset (peer.offset), roundtrip delay (peer.delay) and dispersion (peer.dispersion). it also updates the dispersion of samples that are already recorded and saves the current time (peer.update) โœจ

the heart of the clock-filter procedure is the filter shift register (peer.filter), made up of NTP.SHIFT stages. each stage holds a 3-tuple <$E[ theta sub i ,~delta sub i ,~epsilon sub i ]>, with indices numbered from zero on the left ๐Ÿงš the filter gets initialized with <$E[0,~0,~roman NTP.MAXDISPERSE]> in all stages by the clear procedure. new data samples get shifted in at the left end, which means first NULLs and then old samples fall off the right end. the packet procedure supplies samples of the form (<$Etheta ,~delta ,~epsilon>) as new updates arrive, while the transmit procedure supplies samples of the form <$E[0,~0,~roman NTP.MAXDISPERSE]> when two poll intervals pass without a fresh update ๐Ÿซถ the same symbols (<$Etheta ,~delta ,~epsilon>) are used here for the arguments, the clock-filter contents and the peer variables, but the meaning is clear from context. here's the pseudo-code ๐Ÿ’…

begin clock-filter procedure (<$Etheta ,~delta ,~epsilon>)

okay so the dispersion <$Eepsilon sub i> for all valid samples in the filter register must be updated to account for the skew-error accumulation since the last update ๐Ÿ™ˆ these samples also go on a temporary list with entry format <$E[distance,index]>. the samples in the register get shifted right one stage, the overflow sample is discarded, and the new sample is inserted at the leftmost stage. then the temporary list is sorted by increasing distance. if no samples are left in the list, the procedure exits without updating the peer variables ๐Ÿ’€

        for (i from NTP.SIZE <196> 1 to 1) begin        /* update dispersion */
                <$E[ theta sub i ,~delta sub i ,~epsilon sub i ]~<<-~[ theta sub {i-1} ,~delta sub {i-1} ,~epsilon sub {i-1} ]>;                 /* shift stage right */
                <$Eepsilon sub i~=~epsilon sub i~+~phi tau>;
                add <$E[ lambda sub i~==~epsilon sub i~+~{| delta  sub i |} over 2 ,~i]> to temporary list;
                endfor;
        <$E[ theta sub 0 ,~delta sub 0 ,~epsilon sub 0 ]~<<-~[ theta ,~delta ,~epsilon ]>;                   /* insert new sample */
        add <$E[ lambda~==~epsilon~+~{| delta |} over 2 ,~0]> to temporary list;
        <$Eroman peer.update~<<-~roman sys.clock>;                                                           /* reset base time */
        sort temporary list by increasing <$E[distance~||index]>;

where <$E[distance~||index]> is the concatenation of the distance and index fields, and distance is the high-order field ๐ŸŽ€ the filter dispersion <$Eepsilon sub sigma> gets computed and included in the peer dispersion. note that for this purpose the temporary list is already sorted, so no extra sorting is needed ๐Ÿ’œ

        <$Eepsilon sub sigma~<<-~0>;
        for (i from NTP.SHIFT<196>1 to 0)               /* compute filter dispersion */
                if (<$Eroman peer.dispersion sub index[i]~>>=~roman NTP.MAXDISPERSE> or
                        <$E| theta sub i~-~theta sub 0 |~>>~roman NTP.MAXDISPERSE>)
                        <$Eepsilon sub sigma~<~><<-~( epsilon sub sigma~+~roman NTP.MAXDISPERSE)~times~roman NTP.FILTER>;
                else
                        <$Eepsilon sub sigma~<~><<-~( epsilon sub sigma~+~| theta sub i~-~theta sub 0 |)~times~roman NTP.FILTER>;

last bit for this part ๐ŸŒธ the peer offset <$Etheta sub 0>, delay <$Edelta sub 0> and dispersion <$Eepsilon sub 0> are chosen as the values for the minimum-distance sample. in other words, that's the sample at the first entry on the temporary list, represented here as the 0th subscript โœจ

        <$Eroman peer.offset~<<-~theta sub 0>;

/* update peer variables */
        <$Eroman peer.delay~<<-~delta sub 0>;
        <$Eroman peer.dispersion~<<-~min ( epsilon sub 0~+~epsilon sub
sigma ,~roman NTP.MAXDISPERSE)>;
        end clock-filter procedure

Okay so this is where the peer variables get their final glow-up ๐Ÿ’…. peer.delay gets set from delta sub 0, and peer.dispersion gets the smaller of the two dispersions added together or NTP.MAXDISPERSE, so it never gets too wild ๐ŸŒธ.

Bestie, peer.offset and peer.delay are the two precision queens here. They show the clock offset and roundtrip delay of your local clock compared to the peer clock ๐Ÿซถ. Both can usually be averaged over long stretches to make things more accurate and stable, and the averaging doesn't pile up bias over time (see Appendix H) โœจ. peer.dispersion is the maximum error you could be looking at, and it comes from measurement error, skew-error accumulation and sample variance ๐Ÿ˜ญ. All three get used in clock-selection and clock-combining, so they can pick the best peer clocks for syncing and make the results as accurate and stable as possible ๐Ÿ‘‘.

Clock-Selection Procedure โœจ

The clock-selection procedure is the main character ๐Ÿ’–. It uses the peer variables THETA, DELTA, EPSILON and tau, and it gets called whenever those change or whenever the reachability status changes ๐Ÿฆ‹. It has two algorithms, the intersection algorithm and the clustering algorithm ๐Ÿ”ฅ. The intersection algorithm builds a list of candidate peers that could become the synchronization source, works out a confidence interval for each one, and casts out falsetickers with a technique adapted from Marzullo and Owicki [MAR85] ๐Ÿ’…. The clustering algorithm sorts the surviving candidates by stratum and synchronization distance, then keeps casting out outlyers based on select dispersion until only the most accurate, precise and stable survivors are left ๐ŸŒˆ. Each survivor gets a bit set to show how the selection turned out ๐ŸŽ€. The system variable sys.peer points to the most likely survivor if there is one, and it's set to NULL if there isn't ๐Ÿงš.

Intersection Algorithm ๐Ÿฆ‹

Okay so every peer gets checked one by one, and it only gets added to the endpoint list if it passes some sanity checks. Those checks are there to dodge loops and exceptionally noisy data ๐Ÿ™ˆ. If no peers make it through, the procedure just exits without finding a synchronization source ๐Ÿ’€. For each of m survivors, three entries get added: [THETA-LAMBDA, -1], [THETA, 0] and [THETA+LAMBDA, 1] โœจ. That makes 3m entries total, and the list gets sorted by increasing endpoint ๐Ÿ“.

        m <<- 0;
        for (each peer)                         /* calling all peers */
                if (peer.reach != 0 bold
and peer.dispersion << NTP.MAXDISPERSE} and
                        not (peer.stratum >> 1 and peer.refid =
peer.hostaddr)) begin
                        LAMBDA <MO>
<~>an distance (peer);                 /* make list entry */
                        add [ THETA - LAMBDA , -1] to endpoint list;
                        add [ THETA , 0] to endpoint list;
                        add [ THETA + LAMBDA , 1] to endpoint list;
                        m <<- m + 1;
                        endif
                endfor
        if (m = 0) begin                            /* skedaddle if
no candidates */
                sys.peer <<- NULL;
                sys.stratum <<- 0 (undefined);
                exit;
                endif
        sort endpoint list by increasing endpoint||type;

This next algorithm is adapted from DTS [DEC89], and it's built to find the biggest single intersection that only contains truechimers ๐ŸŒธ. It starts by setting f, i and c to zero ๐Ÿซถ. Then, starting from the lowest endpoint on the sorted list, every entry has its type subtracted from the counter i, which is the number of intersections ๐Ÿ”ฅ. If the type is zero, c goes up by one, and c is the number of falsetickers (see Appendix H) ๐Ÿ’…. If i >= m-f for some entry, that entry's endpoint becomes the lower endpoint of the intersection ๐Ÿฆ‹. If not, f goes up by one and the whole thing runs again ๐Ÿ˜ญ. Without resetting f or c, a similar process finds the upper endpoint, except this time the type gets added to the counter ๐ŸŒˆ. If c <= f after both endpoints are found, the procedure keeps going, having found m-f truechimers ๐Ÿ‘‘. If not, f goes up by one and the entire procedure repeats ๐ŸŽ€.

        for (f from 0 to m over 2) begin              /*
calling all truechimers */
                c <<- 0;
                i <<- 0;
                for (each [endpoint, type] from lowest) begin   /* find
low endpoint */
                        i <<- i - type;
                        low <<- endpoint;
                        if (i >= m-f) break;
                        if (type = 0) c <<- c + 1;
                        endfor;
                i <<- 0;

                for (each [endpoint, type] from highest) begin  /* find
high endpoint */
                        i <<- i + type;
                        high <<- endpoint;
                        if (i >= m-f) break;
                        if (type = 0) c <<- c + 1;
                        endfor;
                if (c <= f) break;                 /* continue
until all falsetickers found */
                endfor;
        if (low > high) begin                              /* quit
if no intersection found */
                sys.peer <<- NULL;
                exit;
                endif;

Note that processing only keeps going past this point if there are more than m/2 intersections ๐ŸŒธ. It's possible, though not super likely, that fewer than m/2 truechimers are left in the intersection ๐Ÿ™ˆ.

Clustering Algorithm ๐Ÿ’–

Okay so in the original DTS algorithm, the procedure exits right here with the presumed correct time set in the middle of the computed intersection [low, high] ๐Ÿ˜ญ. That can cost a lot of accuracy and stability, because the individual peer statistics get lost ๐Ÿ’€. So in NTP, the candidates that survived the earlier steps get processed even further ๐Ÿซถ. The candidate list gets rebuilt with entries of the form [distance, index], where distance comes from the scaled peer stratum and synchronization distance LAMBDA ๐Ÿฆ‹. The scaling factor is how you weight the mix of stratum and distance โœจ. Normally the stratum calls the shots, unless one or more survivors have an exceptionally high distance ๐Ÿ‘‘. Then the list gets sorted by increasing distance ๐Ÿ“.

        m <<- 0;
        for (each peer) begin                   /* calling all peers */
                if (low <<= theta <<= high) begin
                        LAMBDA <<- distance (peer);
/* make list entry */
                        dist <<-
{peer.stratum times NTP.MAXDISPERSE + LAMBDA }
                        add [ dist , peer] to candidate list;
                        m <<- m + 1;
                        endif;
                endfor;
        sort candidate list by increasing dist;

The next steps are all about casting out outlyers that have big dispersions compared to the rest of the candidate list ๐Ÿ”ฅ. They also try to keep wander down, especially on high-speed LANs with loads of time servers ๐ŸŒˆ. Wander causes needless network overhead, because the poll interval gets clamped at sys.poll every time a new peer gets selected for synchronization, and it only creeps up slowly once that peer isn't selected anymore ๐Ÿ˜ญ. The practical experience has been that the number of candidates surviving to this point can get pretty big, and that can eat up a lot of processor cycles without really making things more stable or accurate ๐Ÿ’…. So the candidate list gets truncated at NTP.MAXCLOCK entries ๐ŸŽ€.

Note that epsilon sub xi i is the select (sample) dispersion relative to the ith peer on the candidate list, and you can work it out the same way as the filter dispersion from before ๐Ÿฆ‹. EPSILON sub j is the dispersion of the jth peer on the list, and it includes measurement error, skew-error accumulation and filter dispersion ๐Ÿ’–. If the maximum epsilon sub xi i is bigger than the minimum EPSILON sub j, and there are more survivors than NTP.MINCLOCK, the ith peer gets tossed off the list and the procedure runs again ๐Ÿ™ˆ. If the current synchronization source is one of the survivors and no other survivor has a lower stratum, the procedure exits without doing anything else ๐Ÿ‘‘. Otherwise, the synchronization source gets set to the first survivor on the candidate list โœจ. In what follows, i, j, k and l are peer indices. k is the index of the current synchronization source (NULL if there isn't one), and l is the index of the first survivor on the candidate list ๐Ÿงš.

        while begin
                for (each survivor [distance, index]) begin /*
compute dispersions */
                        find index i for max epsilon sub xi i;
                        find index j for min EPSILON sub j;
                        endfor
                if (epsilon sub xi i <<= EPSILON sub j or
m <<= NTP.MINCLOCK) break;
                peer.survivor [i] <<- 0 ;             /*
discard ith peer */
                if (i = k) sys.peer <<- NULL;
                delete the ith peer from the candidate list;
                m <<- m - 1;
                endwhile
        if (peer.survivor [k] = 0 or peer.stratum
[k] >> peer.stratum [l]) begin
                sys.peer <<- l;
/* new clock source */
                call poll-update;
                endif
        end clock-select procedure;

This algorithm is built to favor the peers near the head of the candidate list ๐ŸŒธ. Those are the ones with the lowest stratum and distance, and they can presumably give the most accurate and stable time ๐Ÿ’…. With a well-chosen weight factor v (also called NTP.SELECT), entries get trimmed from the tail of the list ๐Ÿซถ. The exception is when a few outlyers disagree a lot with the rest, and then those outlyers get discarded first ๐Ÿฆ‹. The termination condition is there to stop pointless switching between synchronization sources when it isn't statistically justified, while still keeping a bias toward the low-stratum, low-distance peers ๐Ÿ‘‘.

Local Clocks ๐ŸŒธ

Okay so to build a precise and accurate local clock, the host needs a hardware clock made of an oscillator and an interface, and that hardware has to be capable of the required precision and stability ๐Ÿซถ. A logical clock gets built on top of those parts, plus some software that adjusts the apparent time and frequency based on periodic updates from NTP or from some other time-sync protocol, like Hellospeak [MIL83b] or the Unix 4.3bsd TSP [GUS85a] ๐Ÿฆ‹. This section describes the Fuzzball local-clock model and implementation ๐Ÿ’–. It includes provisions for precise time and frequency adjustment, and it can keep time within 15 ns and frequency within 0.3 ms per day ๐Ÿ”ฅ. The model works with both compensated and uncompensated quartz oscillators, and it can be adapted to power-frequency oscillators ๐ŸŒˆ. You can find a summary of these and other oscillator types in Appendix E โœจ, and a full mathematical analysis of the NTP local-clock model is in Appendix G ๐Ÿงš.

It is important to note that the particular implementation described here is only one of possibly many implementations that give equivalent functionality ๐ŸŽ€. It's equally important to note that the clock model in Appendix G, which is the basis of the implementation, involves a particular kind of control-feedback loop that is potentially unstable if the design rules are broken ๐Ÿ˜ญ. The model and

The model is suitable for use with both compensated and uncompensated quartz oscillators, and it can be adapted to power-frequency oscillators ๐Ÿ’–. A summary of these and other oscillator types is in Appendix E, and a thorough mathematical analysis of the NTP local-clock model is in Appendix G ๐Ÿง . Note that the implementation described here is only one of possibly many that provide equivalent functionality ๐ŸŽ€. But the clock model in Appendix G, which is the basis of this implementation, uses a control-feedback loop that can become unstable if the design rules are broken ๐Ÿšจ.

The model and parameters in Appendix G are designed to keep time accurate and stable under typical conditions, on ordinary hardware, even when the hardware or network connectivity gets disrupted โœจ. They're tuned for a multi-level hierarchical subnet, where unstable behaviour at one level can knock over possibly many other levels ๐Ÿ’€.

Fuzzball Implementation

The Fuzzball local clock is a bunch of hardware and software registers plus a set of algorithms. Together they make a logical clock that works like a disciplined oscillator and syncs to an external source ๐Ÿฆ‹. Here's how the pieces fit and how they run. All arithmetic is two's complement integer, and all shifts <169><<<<<170> and <169>>>>><170> are arithmetic (sign-fill for right shifts, zero-fill for left shifts) ๐Ÿ”ข. Also, <$Ex~<< <<~n> is the same as <$Ex~>> >>~-~n> ๐Ÿซถ.

The main components are shown in Figure 3 (<$&fig3>), where the fraction points are relative to whole milliseconds ๐Ÿ–ผ๏ธ. The 48-bit Clock register and 32-bit Prescaler work together as a disciplined oscillator that counts milliseconds since midnight, with the fraction point marking the ms boundary ๐Ÿ•›. The 32-bit Clock-Adjust register nudges the oscillator's phase in gentle steps so the timescale never jumps around ๐ŸŽ€. Its contents are called x below. The 32-bit Skew-Compensation register trims the oscillator's frequency by adding small phase increments at regular adjustment intervals, and it can compensate for frequency errors up to .01% or <F128M>๏ฟฝ<F255D>100 ppm ๐Ÿ”ฅ. Its contents are called y below. The 16-bit Watchdog counter and 32-bit Compliance register help decide validity and set the PLL bandwidth and poll interval (see Appendix G) ๐Ÿ‘‘. The Compliance register's contents are called z below. The 32-bit PPS-Adjust register holds a precise time adjustment when a 1-pps pulse source is available, and the 8-bit PPS counter checks that those pulses are actually showing up ๐ŸŒˆ. The two-bit Flags register holds the two leap bits described elsewhere (leap) ๐Ÿ“.

Every register except the Prescaler is normally kept in memory ๐Ÿงš. On typical clock interfaces like the DEC KWV11-C, the Prescaler is a 16-bit buffered counter driven by a quartz oscillator running at some multiple of 1000 Hz โฑ๏ธ. When it overflows, an interrupt fires, and the Clock register gets incremented at the bit that matches the overflow ๐Ÿ’…. To read the time of day, you read the Prescaler (which doesn't interrupt the counting), then add its value to the Clock register, with the fraction points lined up as shown and any unimplemented low-order bits set to zero ๐Ÿซง. On other interfaces, like the LSI-11 event-line mechanism, each tick comes in as an interrupt every 16-2/3 ms or 20 ms, depending on the interface and mains frequency ๐Ÿ””. When that happens, the matching increment in fractional milliseconds gets added to the Clock register ๐Ÿ’œ.

The parameters are summarised in Table 6, where some have been rescaled from Appendix G because this section works in milliseconds ๐Ÿ“Š. When the system starts up, every register and counter is cleared and the leap bits are set to 112 (unsynchronized) ๐ŸŒธ. Every CLOCK.ADJ seconds, CLOCK.ADJ is subtracted from the PPS counter, but only if the PPS counter's previous contents were greater than zero โญ. CLOCK.ADJ is also added to the Watchdog counter, but that counter is clamped so it never goes past NTP.MAXAGE divided by CLOCK.ADJ (one full day) ๐ŸŒ™. And if the Watchdog counter hits that value, the leap bits get set to 112 (unsynchronized) again ๐Ÿ˜ญ.

On some setups you've got a precise timing source: a train of pulses spaced one second apart ๐ŸŽฏ. Usually that comes alongside a timecode source, like a radio clock or even NTP itself, to number the seconds, minutes, hours and days ๐Ÿ•ฐ๏ธ. On typical interfaces like the DEC KWV11-C, there's a special input that fires an interrupt as each pulse arrives ๐Ÿ”Œ. When that happens, the PPS counter is set to CLOCK.PPS and the current time offset is worked out as usual ๐Ÿงฎ. Then the PPS-Adjust register gets that offset scaled to milliseconds ๐Ÿ“. Finally, if PPS-Adjust is greater than or equal to 500, 1000 is subtracted from it ๐Ÿช„. As described below, PPS-Adjust and the PPS counters work with an ordinary timecode to make a super accurate local clock ๐Ÿคฉ.

Gradual Phase Adjustments

If nothing corrects it, the local clock keeps running at whatever offset and frequency it got from its last update ๐Ÿข. An update comes from an event that produces a valid clock selection ๐Ÿ’ซ. It's a signed 48-bit integer in whole milliseconds plus fraction, with the fraction point to the left of bit 32 ๐Ÿ”ข. If its magnitude is bigger than the maximum aperture CLOCK.MAX, a step adjustment is needed, and you proceed as described later ๐Ÿฆถ. Otherwise, you do a gradual phase adjustment ๐ŸŒท. Usually the update comes from the NTP algorithms described earlier, but if the PPS counter is greater than zero, the PPS-Adjust register's value is used instead ๐Ÿ’–. Let u be a 32-bit quantity with bits 0-31 set as bits 16-47 of the update. If any low-order bits of the update aren't implemented, they're set to the sign bit's value ๐Ÿงฉ. These operations move u's fraction point to the left of bit 16, which keeps truncation and roundoff errors small ๐ŸŽฏ. Let b be the number of leading zeros in the absolute value of the Compliance register, and let c be the number of leading zeros in the Watchdog counter. Both are easy to compute with compact loops ๐Ÿ”. Then set b to:

b = b - 16 + CLOCK.COMP

and clamp it so it isn't less than zero ๐Ÿ›ก๏ธ. This is the logarithm of the loop time constant ๐Ÿ“ˆ. Then set c to:

c = 10 - c

and clamp it so it isn't greater than NTP.MAXPOLL <196> NTP.MINPOLL ๐Ÿงท. This is the logarithm of the integration interval since the last update โณ. The clamps keep things stable under the conditions typically seen on the Internet ๐ŸŒ. Then compute new values for the Clock-Adjust and Skew-Compensation registers:

x = u >> b
y = y + (u >> (b + b - c))

Finally, compute the exponential average:

z = z + ((u << (b + CLOCK.MULT) - z) >> CLOCK.WEIGHT)

Here, the left shift realigns the fraction point for better precision and easier computing ๐ŸŽ€.

At each adjustment interval, the final clock correction is made from two parts ๐Ÿซถ. The first, the phase component, is:

x >> CLOCK.PHASE

which gets subtracted from the Clock-Adjust register's previous contents to form its new contents ๐Ÿ’…. The second, the frequency component, is:

y >> CLOCK.FREQ

The phase and frequency components together make the final clock correction, which gets added to the Clock register โœจ. Then the Watchdog counter is set to zero ๐ŸŒธ. This keeps going until a new correction comes in ๐Ÿ”.

The b value worked out above can update the poll interval system variable (sys.poll) ๐Ÿ“‹. It's an adaptive parameter that cuts polling overhead, which is an especially valuable feature when the clock-combining algorithm in Appendix F is used ๐Ÿฏ:

sys.poll = -b + NTP.MINPOLL

When update noise is high, or the hardware oscillator frequency is changing fast because of environmental conditions, the Compliance magnitude goes up ๐ŸŒช๏ธ. With the parameters given, that makes the loop bandwidth (the reciprocal of the time constant) go up and the poll interval go down, eventually to NTP.MINPOLL seconds ๐Ÿƒโ€โ™€๏ธ. When noise is low and the hardware oscillator is very stable, the Compliance goes down, so the loop bandwidth drops and the poll interval grows, eventually to NTP.MAXPOLL seconds ๐Ÿง˜โ€โ™€๏ธ.

Table 6 is tuned so that, in good conditions with updates on the order of a few milliseconds, you can get a precision of a millisecond per day (about .01 ppm or 10-8) ๐Ÿ†. Be careful when implementing this: you need to keep the Clock register monotonic and keep the highest precision while minimising roundoff error propagation ๐Ÿงผ. Since all the real-time multiply and divide operations (except the ones for 1-pps pulses) can be approximated with bitwise shifts, you don't need a full multiply/divide capability in hardware or software ๐Ÿ™Œ.

On many Unix-based systems running NTP, people have consistently found that the single biggest factor in local-clock stability is matching the phase and frequency coefficients to the particular kernel implementation ๐Ÿง. Those coefficients have to be engineered from the model values, or the PLL can fail to track normal oscillator variation and can even go unstable ๐Ÿšจ.

Step Phase Adjustments

When a correction's magnitude is bigger than the maximum aperture CLOCK.MAX, the clock might be so far off from the reference source that the best move is an immediate, full replacement of the Clock register's contents, not gradual adjustments ๐Ÿ’ฅ. But when the sample variance is extremely high, it's wise to be suspicious of a step change, unless a significant interval has passed since the last gradual adjustment ๐Ÿง. So if a step change is indicated and the Watchdog counter is less than the preconfigured value CLOCK.MINSTEP, the update is ignored and the local-clock procedure exits ๐Ÿ›‘. These safeguards are especially useful in setups that use a calibrated atomic clock or LORAN-C receiver along with a separate seconds-numbering source, like a radio clock or NTP peer โš›๏ธ.

If a step change is indicated, the update is added straight to the Clock register, and the Clock-Adjust register and Watchdog counter are both set to zero, but the other registers are left alone ๐Ÿงน. A step change also wipes out the data currently in the clock filters, so the leap bits are set to 112 (unsynchronized) and, as described elsewhere, the clear procedure is called to purge the clock filters and state variables for all peers ๐Ÿงฝ. In practice, a step change is rare and usually happens when the local host or reference source reboots, for example ๐Ÿ”„. That's a relief, because step changes can make the local clock appear to run backward, and they can also produce incorrect delay and offset measurements in the synchronization mechanism itself ๐Ÿ˜ฌ.

Experience on the Internet suggests the CLOCK.MAX values in Table 6 are appropriate ๐Ÿ“š. In practice, a single time-server source only exceeds these values under the most extreme congestion, or when several nodes or links have failed at once ๐ŸŒฉ๏ธ. The most common case is when the time-server source changes and the times indicated by the new and old sources differ by more than the maximum, because of systematic errors in the primary reference source or big differences in path delays ๐Ÿ”€. It's recommended that implementations include provisions for tailoring CLOCK.MAX to specific situations ๐Ÿ› ๏ธ. The amount CLOCK.MAX can be increased without breaking the monotonicity requirement depends on the Clock register increment. For an increment of

10 ms, as used in many workstations, the value shown in Table 6 can be increased by a factor of five. ๐Ÿ’…โœจ

Implementation Issues ๐ŸŽ€

okay so the basic NTP robustness model is that a host has literally no other way to verify time besides NTP itself ๐Ÿ’€. Some equipment has a battery-backed clock/calendar you can use for a sanity check ๐Ÿ”‹. If you've got one, it should be used only to confirm the timekeeping system is sane, and not as the source of system time ๐Ÿ™…โ€โ™€๏ธ. The common assumption (not always justified) is that the clock/calendar is more reliable, but less accurate, than the NTP synchronization subnet, so the recommended approach at initialization is to set the Clock register from the clock/calendar, plus the other registers, counters and flags as described above ๐ŸŒธ. On later updates, if the time offset is greater than a configuration parameter (e.g., 1000 seconds), then the update should be discarded and an error condition reported ๐Ÿšจ. Some implementations periodically save the contents of the Skew-Compensation register to stable storage, like a system file or NVRAM, and pull that value back out at initialization. This can significantly reduce the time it takes to converge to the nominal stability and accuracy regime ๐Ÿš€.

Converting from NTP format to the usual date and time formats that application programs use is way easier if the internal local-clock format keeps separate date and time variables ๐Ÿ—“๏ธ. The time variable is designed to roll over at 24 hours, give or take a leap second as set by the leap-indicator bits. Its overflows (underflows) increment (decrement) the date variable. So the date and time variables show the number of days and seconds since some earlier reference time, but uncorrected for leap seconds that happened in between ๐Ÿงš.

The day before a leap second gets inserted, the leap bits (sys.leap) are set at the primary servers, presumably by hand ๐Ÿ™‹โ€โ™€๏ธ. After that, the bits show up at the local host and get passed to the local-clock procedure. This makes the modulus of the time variable (the length of the current day) go up or down by one second, as appropriate. Right after the insertion, the leap bits are reset. There's more on this in Appendix E ๐Ÿ“–.

Now, the real awkward part is that there's no comprehensive way to administer the leap bits in the primary servers yet ๐Ÿ˜ค. That's better suited to a full network-management mechanism that hasn't been developed yet. In practice, and unless someone has made special provisions, currently manufactured radio clocks have no provisions for leap seconds at all, automatic or manual ๐Ÿ“ป. So when a leap actually happens, the radio has to resynchronize to the broadcast timecode, which can take anywhere from a few minutes to a few hours โณ. Unless special provisions are made, a primary server might leap to the new timescale, only to get yanked back to the old timescale the next time it syncs to the radio. Then the server gets yanked forward again when the radio itself resyncs to the broadcast timecode. It's a whole chaotic cycle ๐ŸŒ€.

This problem can not be reliably avoided using any selection algorithm. There will always be an interval of at least a couple of minutes, and possibly as much as some hours, when some or all radios are out of sync with the broadcast timecode. The subnet only settles down after the majority of them have resynchronized ๐Ÿ˜ญ.

The CLOCK.MINSTEP delay is designed to handle this by forcing a minimum interval since the last gradual adjustment was made before a step change is allowed to happen ๐Ÿ›‘. So until the radio resyncs, it keeps running on the old timescale, which is one second off the local clock after the leap and outside the maximum aperture CLOCK.MAX allowed for gradual phase adjustments ๐Ÿ™ˆ. When the radio finally resyncs, it will almost certainly come up within the aperture and become the reference source again ๐ŸŽ‰. So even in the unlikely case where the local clock incorrectly leaps, the server won't go any longer than CLOCK.MINSTEP seconds before resynchronizing ๐Ÿซถ.


Acknowledgments ๐Ÿซถ

Honestly, so many people helped with this document ๐Ÿฅน. It was thoroughly debated over electronic mail and debugged using two different prototype implementations for the Unix 4.3bsd operating system. One was written by Louis Mamakos and Michael Petry of the University of Maryland, and the other by Dennis Ferguson of the University of Toronto ๐Ÿ‘ฉโ€๐Ÿ’ป. Another implementation, for the Fuzzball operating system [MIL88b], was written by the author ๐Ÿฆ‹. Many individuals, too numerous to mention, meticulously tested the several beta-test prototype versions and ruthlessly smoked out the bugs, both in the code and in the specification ๐Ÿ›๐Ÿ’…. Comments from Dennis Ferguson and Bill Sommerfeld were especially useful, and so were discussions with Joe Comuzzi and others at Digital Equipment Corporation ๐Ÿ’œ.


References ๐Ÿ“š

Okay bestie, here's the full reference list. Every entry is kept exactly as written ๐Ÿง.

[ABA89] Abate, et al. AT&T's new approach to the synchronization of telecommunication networks. IEEE Communications Magazine (April 1989), 35-45.

[ALL74a] Allan, D.W., J.H. Shoaf and D. Halford. Statistics of time and frequency data analysis. In: Blair, B.E. (Ed.). Time and Frequency Theory and Fundamentals. National Bureau of Standards Monograph 140, U.S. Department of Commerce, 1974, 151-204.

[ALL74b] Allan, D.W., J.E. Gray and H.E. Machlan. The National Bureau of Standards atomic time scale: generation, stability, accuracy and accessibility. In: Blair, B.E. (Ed.). Time and Frequency Theory and Fundamentals. National Bureau of Standards Monograph 140, U.S. Department of Commerce, 1974, 205-231.

[BEL86] Bell Communications Research. Digital Synchronization Network Plan. Technical Advisory TA-NPL-000436, 1 November 1986.

[BER87] Bertsekas, D., and R. Gallager. Data Networks. Prentice-Hall, Englewood Cliffs, NJ, 1987.

[BLA74] Blair, B.E. Time and frequency dissemination: an overview of principles and techniques. In: Blair, B.E. (Ed.). Time and Frequency Theory and Fundamentals. National Bureau of Standards Monograph 140, U.S. Department of Commerce, 1974, 233-314.

[BRA80] Braun, W.B. Short term frequency effects in networks of coupled oscillators. IEEE Trans. Communications COM-28, 8 (August 1980), 1269-1275.

[COL88] Cole, R., and C. Foxcroft. An experiment in clock synchronisation. The Computer Journal 31, 6 (1988), 496-502.

[DAR81a] Defense Advanced Research Projects Agency. Internet Protocol. DARPA Network Working Group Report RFC-791, USC Information Sciences Institute, September 1981.

[DAR81b] Defense Advanced Research Projects Agency. Internet Control Message Protocol. DARPA Network Working Group Report RFC-792, USC Information Sciences Institute, September 1981.

[DEC89] Digital Time Service Functional Specification Version T.1.0.5. Digital Equipment Corporation, 1989.

[DER90] Dershowitz, N., and E.M. Reingold. Calendrical Calculations. Software Practice and Experience 20, 9 (September 1990), 899-928.

[FRA82] Frank, R.L. History of LORAN-C. Navigation 29, 1 (Spring 1982).

[GUS84] Gusella, R., and S. Zatti. TEMPO - A network time controller for a distributed Berkeley UNIX system. IEEE Distributed Processing Technical Committee Newsletter 6, NoSI-2 (June 1984), 7-15. Also in: Proc. Summer USENIX Conference (June 1984, Salt Lake City).

[GUS85a] Gusella, R., and S. Zatti. The Berkeley UNIX 4.3BSD time synchronization protocol: protocol specification. Technical Report UCB/CSD 85/250, University of California, Berkeley, June 1985.

[GUS85b] Gusella, R., and S. Zatti. An election algorithm for a distributed clock synchronization program. Technical Report UCB/CSD 86/275, University of California, Berkeley, December 1985.

[HAL84] Halpern, J.Y., B. Simons, R. Strong and D. Dolly. Fault-tolerant clock synchronization. Proc. Third Annual ACM Symposium on Principles of Distributed Computing (August 1984), 89-102.

[JOR85] Jordan, E.C. (Ed). Reference Data for Engineers, Seventh Edition. H.W. Sams & Co., New York, 1985.

[KOP87] Kopetz, H., and W. Ochsenreiter. Clock synchronization in distributed real-time systems. IEEE Trans. Computers C-36, 8 (August 1987), 933-939.

[LAM78] Lamport, L., Time, clocks and the ordering of events in a distributed system. Comm. ACM 21, 7 (July 1978), 558-565.

[LAM85] Lamport, L., and P.M. Melliar-Smith. Synchronizing clocks in the presence of faults. J. ACM 32, 1 (January 1985), 52-78.

[LIN80] Lindsay, W.C., and A.V. Kantak. Network synchronization of random signals. IEEE Trans. Communications COM-28, 8 (August 1980), 1260-1266.

[LUN84] Lundelius, J., and N.A. Lynch. A new fault-tolerant algorithm for clock synchronization. Proc. Third Annual ACM Symposium on Principles of Distributed Computing (August 1984), 75-88.

[MAR85] Marzullo, K., and S. Owicki. Maintaining the time in a distributed system. ACM Operating Systems Review 19, 3 (July 1985), 44-54.

[MIL81a] Mills, D.L. Time Synchronization in DCNET Hosts. DARPA Internet Project Report IEN-173, COMSAT Laboratories, February 1981.

[MIL81b] Mills, D.L. DCNET Internet Clock Service. DARPA Network Working Group Report RFC-778, COMSAT Laboratories, April 1981.

[MIL83a] Mills, D.L. Internet Delay Experiments. DARPA Network Working Group Report RFC-889, M/A-COM Linkabit, December 1983.

[MIL83b] Mills, D.L. DCN local-network protocols. DARPA Network Working Group Report RFC-891, M/A-COM Linkabit, December 1983.

[MIL85a] Mills, D.L. Algorithms for synchronizing network clocks. DARPA Network Working Group Report RFC-956, M/A-COM Linkabit, September 1985.

[MIL85b] Mills, D.L. Experiments in network clock synchronization. DARPA Network Working Group Report RFC-957, M/A-COM Linkabit, September 1985.

[MIL85c] Mills, D.L. Network Time Protocol (NTP). DARPA Network Working Group Report RFC-958, M/A-COM Linkabit, September 1985.

[MIL88a] Mills, D.L. Network Time Protocol (version 1) - specification and implementation. DARPA Network Working Group Report RFC-1059, University of Delaware, July 1988.

[MIL88b] Mills, D.L. The Fuzzball. Proc. ACM SIGCOMM 88 Symposium (Palo Alto, CA, August 1988), 115-122.

[MIL89] Mills, D.L. Network Time Protocol (version 2) - specification and implementation. DARPA Network Working Group Report RFC-1119, University of Delaware, September 1989.

[MIL90] Mills, D.L. Measured performance of the Network Time Protocol in the Internet system. ACM Computer Communication Review 20, 1 (January 1990), 65-75.

[MIL91a] Mills, D.L. Internet time synchronization: the Network Time Protocol. IEEE Trans. Communications 39, 10 (October 1991), 1482-1493.

[MIL91b] Mills, D.L. On the chronology and metrology of computer network timescales and their application to the Network Time Protocol. ACM Computer Communications Review 21, 5 (October 1991), 8-17.

[MIT80] Mitra, D. Network synchronization: analysis of a hybrid of master-slave and mutual synchronization. IEEE Trans. Communications COM-28, 8 (August 1980), 1245-1259.

[NBS77] Data Encryption Standard. Federal Information Processing Standards Publication 46. National Bureau of Standards, U.S. Department of Commerce, 1977.

[NBS79] Time and Frequency Dissemination Services. NBS Special Publication 432, U.S. Department of Commerce, 1979.

[NBS80] DES Modes of Operation. Federal Information Processing Standards Publication 81. National Bureau of Standards, U.S. Department of Commerce, December 1980.

[POS80] Postel, J. User Datagram Protocol. DARPA Network Working Group Report RFC-768, USC Information Sciences Institute, August 1980.

[POS83a] Postel, J. Daytime protocol. DARPA Network Working Group Report RFC-867, USC Information Sciences Institute, May 1983.

[POS83b] Postel, J. Time protocol. DARPA Network Working Group Report RFC-868, USC Information Sciences Institute, May 1983.

[RIC88] Rickert, N.W. Non Byzantine clock synchronization - a programming experiment. ACM Operating Systems Review 22, 1 (January 1988), 73-78.

[SCH86]

[SCH86]

Schneider, F.B. A paradigm for reliable clock synchronization. Department of Computer Science Technical Report TR 86-735, Cornell University, February 1986. ๐Ÿง 

[SMI86]

Smith, J. Modern Communications Circuits. McGraw-Hill, New York, NY, 1986. ๐Ÿ“š

[SRI87]

Srikanth, T.K., and S. Toueg. Optimal clock synchronization. J. ACM 34, 3 (July 1987), 626-645. โœจ

[STE88]

Steiner, J.G., C. Neuman, and J.I. Schiller. Kerberos: an authentication service for open network systems. Proc. Winter USENIX Conference (February 1988). ๐Ÿ”

[SU81]

Su, Z. A specification of the Internet protocol (IP) timestamp option. DARPA Network Working Group Report RFC-781. SRI International, May 1981. ๐ŸŒธ

[TRI86]

Tripathi, S.K., and S.H. Chang. ETempo: a clock synchronization algorithm for hierarchical LANs - implementation and measurements. Systems Research Center Technical Report TR-86-48, University of Maryland, 1986. ๐Ÿฆ‹

[VAN84]

Van Dierendonck, A.J., and W.C. Melton. Applications of time transfer using NAVSTAR GPS. In: Global Positioning System, Papers Published in Navigation, Vol. II, Institute of Navigation, Washington, DC, 1984. ๐Ÿ›ฐ๏ธ

[VAS78]

Vass, E.R. OMEGA navigation system: present status and plans 1977-1980. Navigation 25, 1 (Spring 1978). ๐Ÿงš


Appendix A. NTP Data Format - Version 3 โœจ

okay so the NTP Message data area is the part that sits right after the UDP header ๐ŸŽ€, and it's drawn in Figure 4<$&fig4>. here's the rundown on each field ๐Ÿ’…

Leap Indicator (LI): this is a two-bit code that warns about an impending leap second getting inserted/deleted in the last minute of the current day ๐Ÿ•›, with bit 0 and bit 1, respectively, coded like this:

00, no warning
01, last minute has 61 seconds
10, last minute has 59 seconds)
11, alarm condition (clock not synchronized)

Version Number (VN): this is a three-bit integer showing the NTP version number, which is currently three (3) ๐ŸŒˆ

Mode: this is a three-bit integer showing the mode, with values defined like so ๐Ÿฆ‹

0, reserved
1, symmetric active
2, symmetric passive
3, client
4, server
5, broadcast
6, reserved for NTP control message (see Appendix B)
7, reserved for private use

Stratum: this is an eight-bit integer showing the stratum level of the local clock, with values defined like so ๐Ÿ‘‘

0, unspecified
1, primary reference (e.g.,, radio clock)
2-255, secondary reference (via NTP)

The values that can appear in this field range from zero to NTP.INFIN inclusive. ๐Ÿซถ

Poll Interval: this is an eight-bit signed integer showing the maximum interval between successive messages, in seconds to the nearest power of two. The values that can appear in this field range from NTP.MINPOLL to NTP.MAXPOLL inclusive. โฑ๏ธ

Precision: this is an eight-bit signed integer showing the precision of the local clock, in seconds to the nearest power of two. ๐ŸŽฏ

Root Delay: this is a 32-bit signed fixed-point number showing the total roundtrip delay to the primary reference source, in seconds with the fraction point between bits 15 and 16. ngl this one is wild, because this variable can take on both positive and negative values, depending on clock precision and skew. ๐Ÿ™ˆ

Root Dispersion: this is a 32-bit signed fixed-point number showing the maximum error relative to the primary reference source, in seconds with the fraction point between bits 15 and 16. Only positive values greater than zero are possible. ๐Ÿ“

Reference Clock Identifier: this is a 32-bit code that identifies the particular reference clock. For stratum 0 (unspecified) or stratum 1 (primary reference), this is a four-octet, left-justified, zero-padded ASCII string. It's not enumerated as part of the NTP specification, but the following are suggested ASCII identifiers ๐ŸŒธ

Stratum, Code, Meaning
0, DCN, DCN routing protocol
0, NIST, NIST public modem
0, TSP, TSP time protocol
0, DTS, Digital Time Service
1, ATOM, Atomic clock (calibrated)
1, VLF, VLF radio (OMEGA,, etc.)
1, callsign, Generic radio
1, LORC, LORAN-C radionavigation
1, GOES, GOES UHF environment satellite
1, GPS, GPS UHF satellite positioning

For stratum 2 and greater (secondary reference), this is the four-octet Internet address of the primary reference host. ๐ŸŒ

Reference Timestamp: this is the local time at which the local clock was last set or corrected, in 64-bit timestamp format. ๐Ÿ•ฐ๏ธ

Originate Timestamp: this is the local time at which the request departed the client host for the service host, in 64-bit timestamp format. ๐Ÿš€

Receive Timestamp: this is the local time at which the request arrived at the service host, in 64-bit timestamp format. ๐Ÿ“ฌ

Transmit Timestamp: this is the local time at which the reply departed the service host for the client host, in 64-bit timestamp format. ๐Ÿ’Œ

Authenticator (optional): when the NTP authentication mechanism is implemented, this contains the authenticator information defined in Appendix C. ๐Ÿ”


Appendix B. NTP Control Messages ๐ŸŽ€

In a comprehensive network-management environment, facilities are presumed available to handle routine NTP control and monitoring functions ๐Ÿงš, like setting the leap-indicator bits at the primary servers, adjusting the various system parameters and monitoring regular operations. Ordinarily, these functions can be implemented using a network-management protocol such as SNMP and suitable extensions to the MIB database. However, in those cases where such facilities are not available, these functions can be implemented using special NTP control messages described here. These messages are intended for use only in systems where no other management facilities are available or appropriate, such as in dedicated-function bus peripherals. Support for these messages is not required in order to conform to this specification. ๐Ÿซถ

The NTP Control Message has the value 6 specified in the mode field of the first octet of the NTP header, and it's formatted as shown below. The format of the data field is specific to each command or response ๐Ÿฆ‹. In most cases, though, the format is designed to be constructed and viewed by humans, so it's coded in free-form ASCII. This makes it easier to specify and implement simple management tools when fully evolved network-management facilities aren't around ๐Ÿ’…. As in ordinary NTP messages, the authenticator field follows the data field. If the authenticator is used, the data field is zero-padded to a 32-bit boundary, but the padding bits are not considered part of the data field and are not included in the field count.

IP hosts are not required to reassemble datagrams larger than 576 octets ๐Ÿ™ˆ. However, some commands or responses may involve more data than will fit into a single datagram. So there's a simple reassembly feature where each octet of the message data is numbered starting with zero. As each fragment is transmitted, the number of its first octet goes in the offset field and the number of octets goes in the count field. The more-data (M) bit is set in all fragments except the last. ๐Ÿงฉ

Most control functions involve sending a command and receiving a response, perhaps spread across several fragments ๐Ÿ’–. The sender chooses a distinct, nonzero sequence number and sets the status field and R and E bits to zero. The responder interprets the opcode and the additional information in the data field, updates the status field, sets the R bit to one, and returns the three 32-bit words of the header along with additional information in the data field. In case of invalid message format or contents, the responder inserts a code in the status field, sets the R and E bits to one and, optionally, inserts a diagnostic message in the data field. ๐Ÿšจ

Some commands read or write system variables and peer variables for an association identified in the command. Others read or write variables associated with a radio clock or other device directly connected to a source of primary synchronization information. To tell which type of variable and association is in play, a 16-bit association identifier is used. System variables are indicated by the identifier zero. As each association is mobilized, a unique, nonzero identifier is created for it. These identifiers are used in a cyclic fashion, so the chance of using an old identifier that matches a newly created association is remote ๐Ÿ”„. A management entity can request a list of current identifiers and then use them to read and write variables for each association. An attempt to use an expired identifier results in an exception response, and after that the list can be requested again. ๐Ÿ“‹

Some exception events, such as when a peer becomes reachable or unreachable, happen spontaneously and aren't necessarily tied to a command โšก. An implementation may elect to save the event information for later retrieval, or to send an asynchronous response (called a trap), or both. In the case of a trap, the IP address and port number are determined by a previous command, and the sequence field is set as described below. Current status and summary information for the latest exception event is returned in all normal responses. Bits in the status field indicate whether an exception has occurred since the last response and whether more than one exception has occurred. ๐Ÿ‘€

Commands need not necessarily be sent by an NTP peer, so ordinary access-control procedures may not apply ๐Ÿšง. However, the optional mask/match mechanism suggested elsewhere in this document gives you the power to control access by mode number, so this could be used to limit access for control messages (mode 6) to selected address ranges. main character energy ๐Ÿ‘‘

NTP Control Message Format

The format of the NTP Control Message header, which immediately follows the UDP header, is shown in Figure 5<$&fig5>. Here's the rundown on its fields ๐ŸŒท. Bit positions marked as zero are reserved and should always be transmitted as zero. ๐Ÿ”’

Version Number (VN): this is a three-bit integer showing the NTP version number, which is currently three (3) ๐ŸŒˆ

Mode: this is a three-bit integer showing the mode. It must have the value 6, indicating an NTP control message. ๐Ÿซง

Response Bit (R): set to zero for commands, one for responses. โ†ฉ๏ธ

Error Bit (E): set to zero for a normal response, one for an error response. โŒ

More Bit (M): set to zero for the last fragment, one for all others. ๐Ÿ“ฆ

Operation Code (Op): this is a five-bit integer specifying the command function. The values currently defined include the following ๐Ÿ› ๏ธ

0, reserved
1, read status command/response
2, read variables command/response
3, write variables command/response
4, read clock variables command/response
5, write clock variables command/response
6, set trap address/port command/response
7, trap response
8-31, reserved

Sequence: this is a 16-bit integer showing the sequence number of the command or response. ๐Ÿ”ข

Status: this is a 16-bit code showing the current status of the system, peer or clock, with values coded as described in the following sections. ๐Ÿšฆ

Association ID: this is a 16-bit integer identifying a valid association. ๐Ÿ’œ

Offset ๐ŸŽ€ okay so this is a 16-bit integer telling you the offset, in octets, of the first octet in the data area. Counting from the very start, that's where the good stuff begins โœจ

Count is another 16-bit integer ๐Ÿ’… and it tells you the length of the data field, in octets.

Data is where the message data for the command or response lives ๐Ÿ’œ The maximum number of data octets is 468, so no sneaking in extra!

Authenticator (optional) ๐Ÿ” When the NTP authentication mechanism is implemented, this holds the authenticator information defined in Appendix C ๐Ÿซถ

Status Words ๐Ÿ’–

Status words are giving "what's the vibe right now" ๐Ÿ‘€ They tell you the present status of the system, the associations and the clock. They're designed for network-monitoring programs to read, and they come in one of four 16-bit formats shown in Figure 6<$&fig6> and described in this section.

System and peer status words get attached to responses for all commands except read clock variables, write clock variables and set trap address/port ๐Ÿ™ˆ The association identifier zero gives you the system status word, while a nonzero identifier points to a specific peer association. The status word returned for read clock variables and write clock variables tells you the state of the clock hardware and decoding software. And there's a special error status word for when command fields are malformed or values are invalid ๐Ÿ’€

System Status Word โญ

The system status word shows up in the status field of the response to a read status or read variables command with a zero association identifier. Here's the format:

Leap Indicator (LI) is a two-bit code warning of an impending leap second to be inserted or deleted in the last minute of the current day. Bit 0 and bit 1 are coded like this ๐Ÿฆ‹

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

00, no warning

01, last minute has 61 seconds

10, last minute has 59 seconds)

11, alarm condition (clock not synchronized)

@Z_TBL_END =

Clock Source is a six-bit integer for the current synchronization source ๐ŸŒˆ It's coded like so:

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, unspecified or unknown

1, Calibrated atomic clock (e.g.,, HP 5061)

2, VLF (band 4) or LF (band 5) radio (e.g.,, OMEGA,, WWVB)

3, HF (band 7) radio (e.g.,, CHU,, MSF,, WWV/H)

4, UHF (band 9) satellite (e.g.,, GOES,, GPS)

5, local net (e.g.,, DCN,, TSP,, DTS)
6, UDP/NTP

7, UDP/TIME

8, eyeball-and-wristwatch

9, telephone modem (e.g.,, NIST)

10-63, reserved

@Z_TBL_END =

System Event Counter is a four-bit integer counting the system exception events that happened since the last time the system status word was returned in a response or included in a trap message. It gets cleared when returned in the status field of a response, and it freezes once it hits 15 ๐ŸงŠ

System Event Code is a four-bit integer identifying the latest system exception event. New values overwrite the previous ones ๐Ÿ” and it's coded as follows:

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, unspecified

1, system restart

2, system or hardware fault

3, system new status word (leap bits or synchronization change)

4, system new synchronization source or stratum (sys.peer or sys.stratum
change)

5, system clock reset (offset correction exceeds CLOCK.MAX)

6, system invalid time or date (see NTP specification)

7, system clock exception (see system clock status word)

8-15, reserved

@Z_TBL_END =

Peer Status Word ๐Ÿ“

A peer status word gets returned in the status field of a response to a read status, read variables or write variables command. It also appears in the list of association identifiers and status words returned by a read status command with a zero association identifier. Its format goes like this:

Peer Status is a five-bit code showing the status of the peer as determined by the packet procedure, with bits assigned as follows ๐Ÿ’…

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, configured (peer.config)
1, authentication enabled (peer.authenable)

2, authentication okay (peer.authentic)

3, reachability okay (peer.reach <F128M>๏ฟฝ<F255D> 0)

4, reserved

@Z_TBL_END =

Peer Selection (Sel) is a three-bit integer showing the status of the peer as determined by the clock-selection procedure, with values coded like this ๐Ÿ‘‘

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, rejected

1, passed sanity checks (tests 1 through 8 in Section 3.4.3)

2, passed correctness checks (intersection algorithm in Section 4.2.1)

3, passed candidate checks (if limit check implemented)

4, passed outlyer checks (clustering algorithm in Section 4.2.2)

5, current synchronization source; max distance exceeded (if limit check
implemented)

6, current synchronization source; max distance okay

7, reserved

@Z_TBL_END =

Peer Event Counter is a four-bit integer counting the peer exception events that happened since the last time the peer status word was returned in a response or included in a trap message. It gets cleared when returned in the status field of a response and freezes when it reaches 15 ๐ŸŒธ

Peer Event Code is a four-bit integer identifying the latest peer exception event. New values overwrite previous ones, and it's coded as follows:

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, unspecified

1, peer IP error

2, peer authentication failure (peer.authentic bit was one now zero)

3, peer unreachable (peer.reach was nonzero now zero)

4, peer reachable (peer.reach was zero now nonzero)

5, peer clock exception (see peer clock status word)

6-15, reserved
@Z_TBL_END =

Clock Status Word ๐Ÿงš

There are two ways a reference clock can be attached to an NTP service host: as a dedicated device managed by the operating system, or as a synthetic peer managed by NTP. Just like in read status, the association identifier tells you which one. Zero is for the system clock and nonzero is for a peer clock. Only one system clock is supported by the protocol, though many peer clocks can be supported ๐ŸŒˆ A system or peer clock status word appears in the status field of the response to a read clock variables or write clock variables command. Think of it as an extension of the system status word or the peer status word, whichever fits. The format is:

Clock Status is an eight-bit integer showing the current clock status, with values coded as follows ๐Ÿ•ฐ๏ธ

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, clock operating within nominals

1, reply timeout

2, bad reply format

3, hardware or software fault

4, propagation failure

5, bad date format or value

6, bad time format or value

7-255, reserved

@Z_TBL_END =

Clock Event Code is an eight-bit integer identifying the latest clock exception event, and new values overwrite old ones. When a change to any nonzero value happens in the radio status field, the radio status field gets copied to the clock event code field, and a system or peer clock exception event is declared as appropriate ๐Ÿ”ฅ

Error Status Word ๐Ÿ™ˆ

An error status word comes back in the status field of an error response when the message format or contents are invalid. You know it's there when the E (error) bit is set along with the response (R) bit in the response ๐Ÿšจ It's an eight-bit integer coded like this:

@Z_TBL_BEG = COLUMNS(2), DIMENSION(IN), COLWIDTHS(E1,E8), WIDTH(5.0000),
ABOVE(.0830), BELOW(.0830), HGUTTER(.0560), KEEP(OFF), ALIGN(CT)

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT

0, unspecified

1, authentication failure

2, invalid message length or format
3, invalid opcode

4, unknown association identifier

5, unknown variable name

6, invalid variable value

7, administratively prohibited

8-255, reserved

@Z_TBL_END =

Commands ๐ŸŽ€

Commands consist of the header and an optional data field, as shown in Figure 6. When it's there, the data field holds a list of identifiers or assignments in this form:

<<identifier>>[=<<value>>],<<identifier>>[=<<value>>],...

Here <<identifier>> is the ASCII name of a system or peer variable listed in Table 2 or Table 3, and <<value>> is written as a decimal, hexadecimal or string constant in the syntax of the C programming language. Where there's no ambiguity, you can drop the <169>sys.<170> or <169>peer.<170> prefixes shown in Table 2 or Table 4. Whitespace (ASCII nonprinting format effectors) can be added to make things easier to read for simple monitoring programs that don't reformat the data field โœจ Internet addresses are written as four octets in the form [n.n.n.n], where n is in decimal notation and the brackets are optional. Timestamps, including reference, originate, receive and transmit values, plus the logical clock, are written in units of seconds and fractions, preferably in hexadecimal. Delay, offset, dispersion and distance values are written in units of milliseconds and fractions, preferably in decimal. Everything else is written as-is, preferably in decimal ๐Ÿงฎ

Implementations may define variables other than those in Table 2 or Table 3. These are called extramural variables, and you can spot them because their names include some character other than alphanumeric or <169>.<170>. For commands that return a list of assignments in the response data field, if the command data field is empty, it's expected that all available variables defined in Table 3 or Table 4 of the NTP specification will be included in the response ๐Ÿ“œ For the read commands, if the command data field is nonempty, an implementation may choose to process it so it individually selects which variables get returned.

Here's how the commands are interpreted ๐Ÿ‘‡

Read Status (1): The command data field is empty or contains a list of identifiers separated by commas. This command works in two ways depending on the association identifier. If the identifier is nonzero, the response includes the peer identifier and status word. Optionally, the response data field may contain other info, like described under Read Variables. If the association identifier is zero, the response includes the system identifier (0) and status word, while the data field contains a list of binary-coded pairs

<<association identifier>> <<status word>>,

one for each currently defined association.

Read Variables (2): The command data field is empty or contains a list of identifiers separated by commas. If the association identifier is nonzero, the response includes the requested peer identifier and status word, while the data field contains a list of peer variables and values as described above. If the association identifier is zero, the data field contains a list of system variables and values. If a peer has been selected as the synchronization source, the response includes the peer identifier and status word. Otherwise, the response includes the system identifier (0) and status word ๐Ÿ’–

Write Variables (3): The command data field contains a list of assignments as described above. The variables are updated as indicated.

So the response? Literally the same as the Read Variables command, no cap ๐Ÿ’…

Read Clock Variables (4): okay so this one is all about the clock stuff ๐Ÿ•ฐ๏ธ The command data field is empty or has a list of identifiers separated by commas. The association identifier picks the system clock variables or the peer clock variables, same as in Read Variables โœจ The response includes the requested clock identifier and status word, and the data field holds a list of clock variables and values, including the last timecode message received from the clock ๐Ÿฆ‹

Write Clock Variables (5): the command data field has a list of assignments, just like we saw above ๐ŸŽ€ The clock variables get updated as indicated, and the response is the same as for Read Clock Variables ๐Ÿ’–

Set Trap Address/Port (6): bestie this one is sneaky ๐Ÿ™ˆ The command's association identifier, status, and data fields all get ignored. The address and port for future trap messages come from the source address and port of the control message itself ๐ŸŒธ The starting trap counter comes from the sequence field of the command. The response's association identifier, status, and data fields don't really matter here ๐Ÿงš Implementations should include sanity timeouts, so trap transmissions stop if the monitoring program doesn't renew this info after a long interval โฐ Nobody wants stale traps hanging around forever ๐Ÿ’€

Trap Response (7): this is the alert message ๐Ÿšจ It gets sent when a system, peer, or clock exception event happens. The opcode field is 7 and the R bit is set ๐Ÿ’œ The trap counter goes up by one for each trap sent, and the sequence field is set to that value ๐Ÿ”ข It gets sent using the IP address and port fields established by the set trap address/port command ๐Ÿ“ฎ For a system trap, the association identifier field is set to zero and the status field contains the system status word ๐Ÿ  For a peer trap, the association identifier field is set to that peer and the status field contains the peer status word ๐Ÿ‘ฏ Optional ASCII-coded information can be included in the data field ๐Ÿ“


Appendix C. Authentication Issues โœจ

NTP robustness requirements are super similar to those of other multiple-peer distributed protocols used for network routing, management, and file access ๐ŸŒ We're talking protection from faulty implementations, improper operation, and possibly malicious replay attacks, with or without data modification ๐Ÿ˜ญ These requirements are especially stringent for distributed protocols, because damage from failures can spread super fast, absolutely devastating archives, routes, and monitoring systems and even bringing down major parts of the network, like the classic Internet Worm ๐Ÿ’€๐Ÿ”ฅ

The access-control mechanism suggested in the NTP specification handles these requirements by limiting access to trusted peers ๐Ÿ” The various sanity checks resist most replay and spoofing attacks by discarding old duplicates and using the originate timestamp as a one-time pad, since it's unlikely that even a synchronized peer can predict future timestamps with the precision needed based on past observations alone ๐Ÿง  The protocol environment also resists jamming attacks by using redundant time servers and diverse network paths ๐Ÿ›ค๏ธ Resistance to stochastic disruptions, whether actual or manufactured, is kept to a minimum through careful design of the filtering and selection algorithms ๐ŸŽฏ

However, a determined intruder can still disrupt timekeeping between peers with subtle changes to NTP message data, like falsifying header fields or certain timestamps ๐Ÿ‘€ When protection from even these kinds of attacks is required, a specifically engineered message-authentication mechanism based on cryptographic techniques is necessary ๐Ÿ›ก๏ธ Typical mechanisms involve cryptographic certificates, algorithms, and key media, together with secure media databases and key-management protocols ๐Ÿ—๏ธ Ongoing research in this area is aimed at a standard methodology that can be used with many protocols, including NTP ๐Ÿ”ฌ It may eventually happen that a ubiquitous, widely applicable authentication methodology gets adopted by the Internet community and effectively overtakes the mechanism described here ๐ŸŒ But it doesn't look like specific standards and implementations will show up within the lifetime of this particular version of NTP ๐Ÿซ 

The NTP authentication mechanism described here is meant for interim use, until specific standards and implementations operating at the network level or transport level are available โณ Supporting this mechanism isn't required to conform to the NTP specification itself ๐Ÿ’… The mechanism operates at the application level and is designed to protect against unauthorized message-stream modification and misrepresentation of source, by insuring that unbroken, authenticated paths exist between a trusted, stratum-one server in a particular synchronization subnet and all other servers in that subnet ๐Ÿ”— It uses a crypto-checksum, computed by the sender and checked by the receiver, together with a set of predistributed algorithms, certificates, and cryptographic keys indexed by a key identifier included in the message ๐Ÿ”‘ However, NTP itself has no provisions to distribute or maintain the certificates, algorithms, or keys ๐Ÿ“ญ These quantities may occasionally change, which can result in inconsistent key information while rekeying is in progress ๐Ÿ”„ NTP is pretty tolerant of such disruptions, so no particular provisions are included to deal with them ๐Ÿ˜Œ

The intent of the authentication mechanism is to provide a framework that can be used together with selected mode combinations to build specific plans for managing clockworking communities and implementing policy as needed ๐Ÿ—บ๏ธ It can be selectively enabled or disabled on a per-peer basis ๐ŸŽš๏ธ No specific plan is proposed for managing the use of such schemes, although several possibilities are immediately obvious ๐Ÿ’ก In one scenario, a group of time servers peers among themselves using symmetric modes and shares one secret key, say key 1, while another group of servers peers among themselves using symmetric modes and shares another secret key, say key 2 ๐Ÿ‘ฏ Now, say by policy it's decided that selected servers in group 1 can provide synchronization to group 2, but not the other way around ๐Ÿ™…โ€โ™€๏ธ The selected servers in group 1 are given key 2 but operate only in server mode, so they can't accept synchronization from group 2 ๐Ÿšซ However, group 2 has authenticated access to the group-1 servers ๐Ÿซถ Many other scenarios are possible with suitable combinations of modes and keys ๐ŸŒˆ

A packet format and crypto-checksum procedure appropriate for NTP is specified in the following sections ๐Ÿ“œ The cryptographic information is carried in an authenticator, which follows the (unmodified) NTP header fields ๐ŸŽ The crypto-checksum procedure uses the Data Encryption Standard (DES) [NBS77], but only the DES encryption algorithm is used, and the decryption algorithm isn't necessary ๐Ÿ™ƒ This feature is specifically aimed at governmental sensitivities about exporting cryptographic technology, since the DES decryption algorithm doesn't need to be included in NTP software distributions, so it can't be extracted and used in other applications to avoid message data disclosure ๐Ÿคซ

NTP Authentication Mechanism

When it's created, and possibly at other times, each association is allocated variables identifying the certificate authority, encryption algorithm, cryptographic key, and possibly other data ๐ŸŽ€ The specific procedures to allocate and initialize these variables are beyond the scope of this specification, and so are the association of the identifiers and keys and the management and distribution of the keys themselves ๐Ÿ™Š For example, and for consistency with the conventions of the NTP specification, a set of appropriate peer and packet variables might include the following ๐Ÿ“‹

Authentication Enabled Bit (peer.authenable): this is a bit indicating that the association is to operate in the authenticated mode ๐Ÿ” For configured peers, this bit is determined from the startup environment โš™๏ธ For non-configured peers, this bit is set to one if an arriving message includes the authenticator, and set to zero otherwise ๐Ÿ”ข

Authenticated Bit (peer.authentic): this is a bit indicating that the last message received from the peer has been correctly authenticated โœ…

Key Identifier (peer.hostkeyid, peer.peerkeyid, pkt.keyid): this is an integer identifying the cryptographic key used to generate the message-authentication code ๐Ÿชช The system variable peer.hostkeyid is used for active associations ๐Ÿ“ก The peer.peerkeyid variable is initialized at zero (unspecified) when the association is mobilized ๐ŸŒฑ For authentication purposes, an unassigned value is interpreted as zero (unspecified) 0๏ธโƒฃ

Cryptographic Keys (sys.key): this is a set of 64-bit DES keys ๐Ÿ—„๏ธ Each key is constructed as in the Berkeley Unix distributions, which means eight octets, where the seven low-order bits of each octet correspond to the DES bits 1-7 and the high-order bit corresponds to the DES odd-parity bit 8 ๐Ÿงฉ By convention, the unspecified key 0 (zero), made of eight odd-parity zero octets, is used for testing and is presumed known throughout the NTP community ๐Ÿงช The remaining keys are distributed using methods outside the scope of NTP ๐Ÿ“ฌ

Crypto-Checksum (pkt.check): this is a crypto-checksum computed by the encryption procedure ๐Ÿงฎ

The authenticator field has two subfields: one holding the pkt.keyid variable, and the other holding the pkt.check variable computed by the encrypt procedure ๐ŸŽ The encrypt procedure is called by the transmit procedure described in the NTP specification, and the decrypt procedure is called by the receive procedure described in the NTP specification ๐Ÿ“ž Its presence is revealed by the fact that the total datagram length according to the UDP header is longer than the NTP message length, which includes the header plus the data field, if present ๐Ÿ” For authentication purposes, the NTP message is zero-padded if necessary to a 64-bit boundary, although the padding bits aren't considered part of the NTP message itself ๐Ÿงท The authenticator format shown in Figure 7<$&fig7> has 96 bits, including a 32-bit key identifier and a 64-bit crypto-checksum, and it's aligned on a 32-bit boundary for efficient computation ๐Ÿ—๏ธ Additional information required in some implementations, like certificate authority and encryption algorithm, can be inserted between the (padded) NTP message and the key identifier, as long as the alignment conditions are met ๐Ÿงฑ Like the authenticator itself, this information isn't included in the crypto-checksum ๐Ÿ™…โ€โ™€๏ธ Use of this data is beyond the scope of this specification ๐Ÿšง These conventions may be changed in the future as the result of other standardization activities ๐Ÿ”ฎ

NTP Authentication Procedures

When authentication is implemented, there are two additional procedures added to the ones described in the NTP specification ๐Ÿง One of them (encrypt) constructs the crypto-checksum in transmitted messages, while the other (decrypt) checks this quantity in received messages ๐Ÿ” The procedures use a variant of the cipher-block chaining method described in [NBS80] as applied to DES ๐Ÿ”— In principal, the procedure is independent of DES and only requires that the encryption algorithm operate on 64-bit blocks ๐Ÿง  While the NTP authentication mechanism specifies the use of DES, other algorithms could be used by prior arrangement ๐Ÿค

Encrypt Procedure

For ordinary NTP messages, the encryption procedure works like this ๐Ÿ“ If authentication is not enabled, the procedure simply exits ๐Ÿšช If the association is active (modes 1, 3, 5), the key is determined from the system key identifier ๐Ÿ“ก If the association is passive (modes 2, 4), the key is determined from the peer key identifier if the authentic bit is set, or as the default key (zero) otherwise ๐Ÿ”‘ These conventions allow further protection against replay attacks and keying errors, and they make testing and migration to new versions easier ๐Ÿ›ก๏ธ The crypto-checksum is

calculated using the 64-bit NTP header and data words (but not the authenticator or padding bits) ๐ŸŽ€

Here's the encrypt procedure in full, so you can see every single step ๐Ÿ’…

begin encrypt procedure
        if (<$Eroman peer.authenable~=~0>) exit;                /* do
nothing if not enabled */
        if (<$Eroman {peer.hostmode~=~1~bold or~peer.hostmode~=~3~bold
or~peer.hostmode ~=~5}>)
                <$Ekeyid~<<-~roman peer.hostkeyid>;             /*
active modes use system key */
        else
                if (<$Eroman peer.authentic~=~1>)               /*
passive modes use peer key */
                         <$Ekeyid~<<-~roman peer.peerkeyid>;
                else
                         <$Ekeyid~<<-~0>;                       /*
unauthenticated use key 0 */
        <$Etemp~<<-~0>;                                 /* calculate
crypto-checksum */
        for (each 64-bit header and data word) begin
                <$Etemp~<<-~temp~roman bold xor~word>;
                <$Etemp~<<-~roman DES (temp,~keyid)>;
                endfor;
        <$Eroman pkt.keyid~<<-~keyid>;                          /*
insert packet variables */
        <$Eroman pkt.check~<<-~temp>;
        end encrypt procedure;

Decrypt Procedure

Okay so for ordinary messages, the decrypt procedure goes like this ๐Ÿฆ‹ If the peer is not configured, the data portion of the message gets inspected to see whether the authenticator fields are there. If they are, authentication is enabled; if not, it's disabled. If authentication is enabled, the authenticator fields are present, and the crypto-checksum succeeds, the authentication bit gets set to one. Otherwise it gets set to zero. Iconic behavior, honestly โœจ

begin decrypt procedure
        <$Eroman peer.authentic~<<-~0>;
        if (<$Eroman peer.config~=~0>)                          /* if
not configured, enable per packet */
                if (authenticator present)
                        <$Eroman peer.authenable~<<-~1>;
                else
                        <$Eroman peer.authenable~<<-~0>;
        if (<$Eroman peer.authenable~=~0> or authenticator not present))
exit;
        <$Eroman {peer.peerkeyid~<<-~pkt.keyid}>;               /* use
peer key */
        <$Etemp~<<-~0>;                                 /* calculate
crypto-checksum */
        for (each 64-bit header and data word) begin
                <$Etemp~<<-~temp~roman bold xor~word>;
                <$Etemp~<<-~roman DES (temp,~roman peer.peerkeyid)>;
                endfor;
        if (temp == pkt.check) <$Eroman peer.authentic~<<-~1>;  /*
declare result */
        end decrypt procedure;

Control-Message Procedures

Now the control messages ๐Ÿซถ Since these functions are expected to eventually get folded into a bigger network-management function, the peer variables are NOT used for control message authentication. If an NTP command message comes in with an authenticator field, the crypto-checksum is computed the same way as in the decrypt procedure, and the response message includes an authenticator field computed by the encrypt procedure. If the received authenticator is correct, the response uses the same key as the command. If it isn't, the default key (zero) gets used instead. Commands that change the peer database, like the write variables and set trap address/port commands, must be correctly authenticated ๐Ÿ”ฅ The rest of the commands are normally not authenticated, just to keep the encryption overhead low ๐Ÿ“


Appendix D. Differences from Previous Versions โœจ

Okay bestie, history time ๐Ÿงš The original NTP, which later got called NTP Version 0, was described in RFC-958 [MIL85c]. Version 0 then got superseded by Version 1 (RFC-1059 [MIL88a]), and Version 2 (RFC-1119 [MIL89]. The Version-2 description was split into two documents. RFC-1119 defines the architecture and specifies the protocol and algorithms, and another document [MIL90b] describes the service model, algorithmic analysis and operating experience. In earlier versions, all of that lived in one document. Version 3 uses the same architecture as Version 2, but the protocols and algorithms differ in minor ways ๐Ÿ’œ The differences between NTP Version 3 and the earlier versions are described in this Appendix. Because of known bugs in very old implementations, continued support for Version-0 implementations is not recommended. It is recommended that new implementations follow the guidelines below when interoperating with older implementations.

Version 3 doesn't change the protocol in any significant way, and it doesn't obsolete previous versions or existing implementations ๐ŸŒˆ The main motivation is to refine the analysis and implementation models for new applications running at much higher network speeds, into the gigabit-per-second regime, and to provide the stability, accuracy and precision that speeds like that need. Specifically, the sources of time and frequency errors have been rigorously examined, and error bounds have been established. That helps improve performance, gives a model for correctness assertions, and lets the software indicate timekeeping quality to the user. Version 3 also adds two new optional features: (1) an algorithm that combines the offsets of several peer time servers to boost accuracy, and (2) improved local-clock algorithms that let the poll intervals on all synchronization paths get substantially longer, which cuts down on network overhead. Here's a summary of the previous versions of the protocol, plus the details of the Version 3 changes ๐ŸŽ€

  1. Version 1 supports no modes other than symmetric-active and symmetric-passive. Those are determined by looking at the port-number fields of the UDP packet header. The peer mode can be determined explicitly from the packet-mode variable (pkt.mode) if it's nonzero, and implicitly from the source port (pkt.peerport) and destination port (pkt.hostport) variables if it's zero. When pkt.mode is zero, the mode is determined as follows:
@Z_TBL_BEG = COLUMNS(3), DIMENSION(IN), WIDTH(5.0000), ABOVE(.1670),
BELOW(.0830), HGUTTER(.3330), BOX(Z_SINGLE), KEEP(ON), ALIGN(CT),
L1(R1C0..R1C3)

@Z_TBL_BODY = TABLE HEADER, TABLE HEADER, TABLE HEADER

pkt.peerport, pkt.hostport, Mode

@Z_TBL_BODY = TABLE TEXT, TABLE TEXT, TABLE TEXT

NTP.PORT, NTP.PORT, symmetric active

NTP.PORT, not NTP.PORT, server

not NTP.PORT, NTP.PORT, client

@Z_TBL_BODY = TABLE HEADER, TABLE HEADER, TABLE HEADER

not NTP.PORT, not NTP.PORT, not possible

@Z_TBL_END =

Heads up: in this case it isn't possible to tell symmetric active apart from symmetric passive ๐Ÿ™ˆ Using pkt.mode and NTP.PORT this way is not recommended, and it may not be supported in future versions of the protocol. The low-order three bits of the first octet were specified as zero in Version 1, but Version 2 uses them for the mode field. Version-2 and Version-3 implementations that interoperate with Version-1 implementations should operate in a passive mode only, and should use the value one in the version number (pkt.version) field and zero in the mode (pkt.mode) field in transmitted messages.

  1. Version 1 doesn't support the NTP control message described in Appendix B. Certain old versions of the Unix NTP daemon ntpd use the high-order bits of the stratum field (pkt.stratum) for control and monitoring. Those bits are never set during normal Version-1, Version-2 or Version-3 operation. New implementations may use the NTP reserved mode 6 described in Appendix B, and/or private reserved mode 7, for special purposes like remote control and monitoring. In those cases, the format of the packet after the first octet can be arbitrary ๐Ÿฆ‹ Different implementations aren't guaranteed to interoperate using private reserved mode 7, but it is recommended that vanilla ASCII format be used whenever possible.
  2. Version 1 doesn't support authentication. The key identifiers, cryptographic keys and procedures described in Appendix C are new in Version 2 and continue into Version 3, along with the corresponding variables, procedures and authenticator fields. In the NTP message described in Appendix A and the NTP control message described in Appendix B, the format and contents of the header fields are independent of the authentication mechanism. The authenticator itself follows the header fields, so previous versions just ignore it ๐Ÿ’–
  3. In Version 1, the total dispersion (pkt.rootdispersion) field of the NTP header was called the estimated drift rate, and it wasn't used in the protocol or timekeeping procedures. Version-1 implementations typically set this field to the current value of the skew-compensation register, which is a signed quantity. In a Version-2 implementation, apparently large values in this field may affect the order considered in the clock-selection procedure. Version-2 and Version-3 implementations interoperating with older implementations should assume this field is zero, no matter what its actual contents are ๐ŸŒธ
  4. Version 2 and Version 3 include several sanity checks designed to avoid disruptions caused by unsynchronized, duplicate or bogus timestamp information. The checks in Version 3 are specifically designed to detect lost or duplicate packets and to resist invalid timestamps. The leap-indicator bits are set to show the unsynchronized state if updates aren't received from a reference source for a considerable time, or if the reference source hasn't received updates for a considerable time. Some Version-1 implementations could claim valid synchronization indefinitely after losing the reference source ๐Ÿ˜ญ
  5. The clock-selection procedure of Version 2 got a big glow-up from accumulated experience with the Version-1 implementation ๐Ÿ‘‘ Additional sanity checks were added for authentication, range bounds, and avoiding very old data. The candidate list gets sorted twice: once to pick a relatively small set of robust candidates from a potentially huge population of unruly peers, and again to order the resulting list by measurement quality. As in Version 1, the final selection procedure repeatedly casts out outlyers based on weighted dispersion.
  6. The local-clock procedure of Version 2 was considerably improved over Version 1, thanks to analysis, simulation and experience โญ Checks were added to warn when the oscillator has gone too long without an update from a reference source. The compliance register was added to improve frequency stability to the order of a millisecond per day. The various parameters were retuned for optimum loop stability, using measured data over typical Internet paths and with typical local-clock hardware. In Version 3, the phase-lock loop model was refined further to provide an adaptive-bandwidth feature. It automatically adjusts for the inherent stabilities of the reference clock and the local clock, while still providing optimum loop stability in each case ๐Ÿงš
  7. Problems in the timekeeping calculations of Version 1 with high-speed LANs were found and corrected in Version 2. They were caused by jitter from small differences in clock rates and different precisions between the peers. Subtle bugs in the Version-1 reachability and polling-rate control were also found and fixed. The peer.valid and sys.hold variables were added to avoid instabilities when the reference source changes rapidly because of large dispersive delays under severe network congestion. The peer.config, peer.authenable and peer.authentic bits were added to control special features and simplify configuration ๐Ÿ“
  8. In Version 3, the local-clock algorithm was overhauled to improve stability and accuracy ๐Ÿ’… Appendix G presents a detailed mathematical model and design example, refined with feedback-control analysis and extensive simulation using data collected over ordinary Internet paths. Section 5 of RFC-1119 on the NTP local clock was completely rewritten to describe the new algorithm. Since the new algorithm can result in message rates far below the old ones, it

is highly recommended that they be used in new implementations, bestie ๐Ÿ’– Just so you know, this algorithm isn't part of the NTP protocol spec itself, and using it doesn't affect interoperability with previous versions or existing implementations ๐Ÿซถ BUT to keep the overall NTP subnet stable on the Internet, it is essential that the local-clock characteristics of all NTP time servers match the analytical models presented earlier and in this document ๐Ÿ”ฅ

10.

In Version 3 there's a brand new way to combine the offsets from a bunch of peer time servers ๐Ÿ’…, presented in Appendix F. It's modelled on what national standards labs do: they combine the weighted offsets from a number of standard clocks to build a synthetic laboratory timescale that's more accurate than any single clock on its own ๐Ÿงš. It can be used in an NTP implementation to improve accuracy and stability and reduce errors due to asymmetric paths in the Internet ๐ŸŒˆ. The new algorithm was simulated using data collected over ordinary Internet paths and, along with the new local-clock algorithm, was implemented and tested in the Fuzzball time servers now running in the Internet ๐Ÿ”ฅ. Note that this algorithm is not integral to the NTP protocol specification itself and its use does not affect interoperability with previous versions or existing implementations ๐Ÿ™ˆ.

11.

Several inconsistencies and minor errors from previous versions got fixed in Version 3 ๐Ÿ˜ญ๐ŸŽ€. The procedures were rewritten in pseudo-code with English commentary so everything is clear and there's no ambiguity (it was a long time coming). Appendix I was added to show C-language implementations of the various filtering and selection algorithms suggested for NTP ๐Ÿ’œ. Section 5 and Appendix E also have more info. Appendix E includes the tutorial material that used to be in Section 2 of RFC-1119, plus lots of new material clarifying how to interpret timescales and leap seconds ๐Ÿฆ‹.

12.

Minor changes were made to the Version-3 local-clock algorithms to avoid problems seen when leap seconds get introduced in the UTC timescale, and to support an auxiliary precision oscillator, like a cesium clock or timing receiver, as a precision timebase โœจ. There were also changes to some procedures in Section 3 and to the clock-filter and clock-selection procedures in Section 4. These fixed minor bugs found through experience, and they're recommended for new implementations ๐Ÿ’–. They don't affect interoperability with previous versions or existing implementations in other than minor ways (at least until the next leap second) ๐Ÿ’€.

13.

In Version 3 the way delay, offset and dispersion are defined, calculated and processed changed so the errors inherent in the time-transfer procedures get reliably bounded ๐Ÿซถ. Specifically, the error accumulations moved from the delay computation to the dispersion computation, and both are included in the clock filter and selection procedures. The clock-selection procedure dropped the first of its two sorting/discarding steps and swapped in an algorithm first proposed by Marzullo and later incorporated in the Digital Time Service ๐Ÿ‘‘. These changes don't significantly affect ordinary operation or compatibility with various versions of NTP, but they do provide the basis for formal statements of correctness, as described in Appendix H ๐Ÿ“.


Appendix E. The NTP Timescale and its Chronometry โœจ

Introduction

Okay bestie, this is an extended deep dive into computer network chronometry ๐Ÿ•ฐ๏ธ. That's the precise determination of computer time and frequency relative to international standards, plus figuring out conventional civil time and date on the modern calendar ๐Ÿ“…. It covers the methods people conventionally use to establish civil time and date and the various timescales in use now ๐ŸŒธ. Mainly it characterizes the Network Time Protocol (NTP) timescale relative to the Coordinated Universal Time (UTC) timescale, and it establishes the precise interpretation of UTC leap seconds in NTP โœจ.

In this discussion, the terms time, oscillator, clock, epoch, calendar, date and timescale are used in a technical sense ๐Ÿ’…. Strictly speaking, the time of an event is an abstraction that determines the ordering of events in some given frame of reference. An oscillator is a generator capable of a precise frequency (relative to that frame of reference) to a specified tolerance. A clock is an oscillator together with a counter that records the (fractional) number of cycles since being initialized with a given value at a given time. The value of the counter at any given time is called its epoch at that time. In general, epoches are not continuous and depend on the precision of the counter ๐Ÿงฎ.

A calendar is a mapping from epoch in some frame of reference to the times and dates used in everyday life ๐Ÿ“†. Since multiple calendars are in use today and sometimes disagree on the dating of the same events in the past, the chronometry of past and present events is an art practiced by historians ๐Ÿง. One goal of this discussion is to provide a standard chronometry for precision dating of present and future events in a global networking community ๐ŸŒ. To synchronize frequency means adjusting the oscillators in the network to run at the same frequency. To synchronize time means setting the clocks so that all agree at a particular epoch with respect to UTC, as provided by international standards. To synchronize clocks means synchronizing them in both frequency and time ๐Ÿ’œ.

In order to synchronize clocks, there must be some way to directly or indirectly compare them in time and frequency ๐Ÿ”ญ. The ultimate frame of reference for our world consists of the cosmic oscillators: the Sun, Moon and other galactic orbiters โ˜€๏ธ๐ŸŒ™. Since the frequencies of these oscillators are relatively unstable and not known exactly, the ultimate reference standard oscillator has been chosen by international agreement as a synthesis of many observations of an atomic transition of exquisite stability ๐Ÿ’Ž. The epoches of each heavenly and Earthbound oscillator define a distinctive timescale, not necessarily always continuous, relative to the standard oscillator. Another goal is to describe a standard chronometry that rationalizes conventional computer time and UTC, in particular how to handle leap seconds ๐Ÿซง.

Primary Frequency and Time Standards

A primary frequency standard is an oscillator that can maintain extremely precise frequency relative to a physical phenomenon, such as a transition in the orbital states of an electron โš›๏ธ. Presently available atomic oscillators are based on the transitions of the hydrogen, cesium and rubidium atoms ๐Ÿงช. Table 7<$&tab7> shows the characteristics for typical oscillators of these types compared with those for various types of quartz-crystal oscillators found in electronic equipment. For reasons of cost and robustness, cesium oscillators are used worldwide for national primary frequency standards ๐Ÿ›๏ธ. Local clocks used in computing equipment, on the other hand, almost always are designed with uncompensated crystal oscillators ๐Ÿ’ป.

For the three atomic oscillators listed in Table 7, the drift/aging column shows the maximum offset per day from nominal standard frequency due to systematic mechanical and electrical characteristics. For crystal oscillators this offset isn't constant, which results in a gradual change in frequency with time, called aging ๐Ÿข. Even if a crystal oscillator is temperature compensated by some means, it must be periodically compared to a primary standard to maintain the highest accuracy ๐ŸŽฏ. For all types of oscillators, the stability column shows the maximum variation in frequency per day due to circuit noise and environmental factors ๐ŸŒก๏ธ.

As telephone networks around the world evolve rapidly to digital technology, we should think about the methods used for frequency synchronization in digital networks ๐Ÿ“ž. A network of clocks in which each oscillator is phase-locked to a single frequency standard is called isochronous ๐Ÿ’ซ. A network in which some oscillators are phase-locked to different master oscillators, with the master oscillators closely synchronized in frequency (not necessarily phase locked) to a single frequency standard, is called plesiochronous ๐Ÿฆ‹. In plesiochronous systems, the phase of some oscillators can slip relative to others and cause occasional data errors in synchronous transmission systems ๐Ÿ˜ฌ.

The industry has agreed on a classification of clock oscillators as a function of minimum accuracy, minimum stability and other factors [ALL74a] ๐Ÿ“‹. There are three factors which determine the classification: stability, jitter and wander. Stability refers to the systematic variation of frequency with time and is synonymous with aging, drift, trends, etc. Jitter (also called timing jitter) refers to short-term variations in frequency with components greater than 10 Hz, while wander refers to long-term variations in frequency with components less than 10 Hz ๐ŸŒŠ. The classification determines the oscillator stratum (not to be confused with the NTP stratum ๐Ÿ™ƒ), with the more accurate oscillators assigned the lower strata and the less accurate oscillators the higher strata:

@Z_TBL_BEG = COLUMNS(3), DIMENSION(IN), COLWIDTHS(E1,E2,E2),
WIDTH(5.0000), ABOVE(.1670), BELOW(.0830), HGUTTER(.3330),
BOX(Z_SINGLE), KEEP(ON), ALIGN(CT), L1(R1C0..R1C3)

@Z_TBL_BODY = TABLE CENTER, TABLE HEADER, TABLE HEADER

Stratum, Min Accuracy (per day), Min Stability (per day)

@Z_TBL_BODY = TABLE CENTER, TABLE TEXT, TABLE TEXT

1, 1 x 10-11, not specified

2, 1.6 x 10-8, 1 x 10-10

3, 4.6 x 10-6, 3.7 x 10-7

@Z_TBL_BODY = TABLE CENTER, TABLE HEADER, TABLE HEADER

4, 3.2 x 10-5, not specified

@Z_TBL_END =

The construction, operation and maintenance of stratum-one oscillators is assumed to be consistent with national standards, and often includes cesium oscillators or precision crystal oscillators synchronized via LORAN-C to national standards ๐Ÿ›ฐ๏ธ. Stratum-two oscillators represent the stability required for interexchange toll switches such as the AT&T 4ESS and interexchange digital cross-connect systems, while stratum-three oscillators represent the stability required for exchange switches such as the AT&T 5ESS and local cross-connect systems. Stratum-four oscillators represent the stability required for digital channel-banks and PBX systems ๐Ÿ“ .

Time and Frequency Dissemination

So that atomic and civil time can be coordinated throughout the world, national administrations operate primary time and frequency standards and coordinate them cooperatively by observing various radio broadcasts and through occasional use of portable atomic clocks ๐ŸŒ. Most seafaring nations operate some sort of broadcast time service for the purpose of calibrating chronographs, which are used in conjunction with ephemeris data to determine navigational position โ›ต. In many countries the service is primitive and limited to seconds-pips broadcast by marine communication stations at certain hours ๐Ÿ•. For instance, a chronograph error of one second represents a longitudinal position error of about 0.23 nautical mile at the Equator ๐Ÿงญ.

The U.S. National Institute of Standards and Technology (NIST - formerly National Bureau of Standards) operates three radio services for the dissemination of primary time and frequency information ๐Ÿ‡บ๐Ÿ‡ธ. One of these uses high-frequency (HF or CCIR band 7) transmissions on frequencies of 2.5, 5, 10, 15 and 20 MHz from Fort Collins, CO (WWV), and Kauai, HI (WWVH) ๐Ÿ“ป. Signal propagation is usually by reflection from the upper ionospheric layers, which vary in height and composition throughout the day and season and result in unpredictable delay variations at the receiver ๐ŸŒŒ. The timecode is transmitted over a 60-second interval at a data rate of 1 bps using a 100-Hz subcarrier on the broadcast signal ๐ŸŽถ.

The timecode info has UTC time-day stuff, but it doesn't include the year or a leap-second heads-up yet ๐Ÿ™ˆ These transmissions, plus the ones from Canada in Ottawa, Ontario (CHU) and other countries, can be picked up over a huge chunk of the western hemisphere ๐ŸŒŽ. Reliable frequency comparisons only get you to about 10-7, though, and time accuracy is stuck around a millisecond [BLA74]. Radio clocks that work with these include the Traconex 1020, which gets you to about ten milliseconds and costs around $1,500 ๐Ÿ’ธ

NIST's second service is LF (CCIR band 5) on 60 kHz from Boulder, CO (WWVB), and it can be received across the continental U.S. and nearby coastal areas ๐Ÿ‡บ๐Ÿ‡ธ The signal bounces off the lower ionospheric layers, which are pretty stable and have predictable daily changes in height. The timecode goes out over a 60-second interval at 1 pps, using periodic dips in carrier power. With good receiving and averaging techniques, plus corrections for diurnal and seasonal propagation effects, frequency comparisons within 10-11 are possible and time accuracies from a few to 50 microseconds are doable [BLA74] โœจ Some western European countries run similar services: 60 kHz from Rugby, U.K. (MSF), and 77.5 kHz from Mainflingen, West Germany (DCF77). Their timecode includes UTC time-day-year info and leap-second warning ๐Ÿซถ Radio clocks for these include the Spectracom 8170 and the Kinemetrics/TrueTime 60-DC and LF-DC, which give accuracies to a millisecond or less and cost around $2,500. Lowkey annoying part: these receivers don't extract the year info or the leap-second warning ๐Ÿ˜ญ

NIST's third service is UHF (CCIR band 9) on about 468 MHz from the Geosynchronous Orbit Environmental Satellites (GOES), and three of them cover the western hemisphere ๐Ÿ›ฐ๏ธ The timecode is interleaved with messages used to interrogate remote sensors. It's 60 4-bit binary-coded decimal words sent over 30 seconds. It includes UTC time-day-year info and leap-second warning, but the Kinemetrics/TrueTime 468-DC, which gets to 0.5 ms and costs around $6,000, doesn't extract the year info or the leap-second warning ๐Ÿ’€

The U.S. Department of Defense is building the Global Positioning System (GPS) for worldwide precision navigation ๐Ÿงญ Eventually it'll give 24-hour worldwide coverage from a constellation of 24 satellites in 12-hour orbits. For time transfer, GPS has a potential accuracy in the order of a few nanoseconds, but various defense policy considerations may limit it to hundreds of nanoseconds [VAN84] ๐Ÿ™ƒ The timecode includes GPS time and a UTC correction, though there appears to be no leap-second warning. The Kinemetrics/TrueTime GPS-DC gets accuracy to 200 <$Emu>s and costs around $12,000. Since only about half the satellites are up so far, you need expensive rubidium or quartz oscillators to keep accuracy during outages. Also, it's a single-channel receiver, so it must be given geographic coordinates within a degree from an external source before it starts up ๐Ÿ—บ๏ธ

The U.S. Coast Guard, along with agencies from other countries, has run the LORAN-C [FRA82] radionavigation system for years ๐ŸŒŠ It currently gives time-transfer accuracy of less than a microsecond, and eventually it may hit 100 ns within the ground-wave coverage area of a few hundred kilometers from the transmitter. Past the ground wave, the signal bounces off the lower ionospheric layers, which drops accuracy to the order of 50 us. With the recent addition of the Mid-Continent Chain, LORAN-C transmitters now give complete U.S. coverage. The LORAN-C timing receivers, like the Austron 2000, are specialized and extremely expensive (up to $20,000). They're mainly used to monitor local cesium clocks and aren't built for unattended, automatic operation ๐Ÿ™…โ€โ™€๏ธ LORAN-C gives a super accurate frequency and time reference inside the ground-wave area, but there's no timecode modulation, so the receiver must be given UTC to within a few tens of seconds from an external source before it starts โฑ๏ธ

The OMEGA [VAS78] radionavigation system, run by the U.S. Navy and other countries, uses eight very-low-frequency (VLF or CCIR band 4) transmitters on frequencies from 10.2 to 13.1 kHz, with 24-hour worldwide coverage ๐ŸŒ With the right receiving and averaging techniques and corrections for propagation effects, frequency comparisons and time accuracies are comparable to the LF systems, just with worldwide coverage [BLA74] ๐Ÿ’œ The Kinemetrics/TrueTime OM-DC gets accuracy to 1 ms and costs around $3,500. OMEGA gives a highly accurate frequency reference, but there's no timecode modulation, so the receiver must be given geographic coordinates within a degree and UTC within five seconds from an external source before it starts. There are also several other VLF services mainly meant for worldwide data communications, with characteristics similar to OMEGA. They can be used kind of like OMEGA, but that needs specialized techniques that aren't suited for unattended, automatic operation ๐Ÿซ 

Note that not all transmission formats used by NIST radio broadcast services [NBS79] and no currently available radio clocks include provisions for year info and leap-second warning ๐Ÿ“Œ That info has to come from other sources. NTP includes provisions to distribute advance warnings of leap seconds using the leap-indicator bits described in the NTP specification. The protocol is designed so those bits can be set manually or by the radio timecode at the primary time servers, and then automatically distributed throughout the synchronization subnet to all other time servers ๐Ÿ”ฅ

Calendar Systems

Okay, calendar history time ๐Ÿ“œ The calendar systems used in the ancient world reflect the agricultural, political and ritual needs of the societies that flourished with them ๐ŸŒพ People were doing astronomical observations to pin down the winter and summer solstices three to four millennia ago โ˜€๏ธ By the 14th century BC, the Shang Chinese had worked out the solar year as 365.25 days and the lunar month as 29.5 days ๐ŸŒ™ The lunisolar calendar, where the ritual month follows the Moon and the agricultural year follows the Sun, was used all over the ancient Near East (except Egypt) and Greece from the third millennium BC. Early calendars used either thirteen lunar months of 28 days or twelve alternating lunar months of 29 and 30 days, plus some pretty haphazard methods to reconcile the 354/364-day lunar year with the 365-day vague solar year ๐Ÿคทโ€โ™€๏ธ

The ancient Egyptian lunisolar calendar had twelve 30-day lunar months, but it was guided by the seasonal appearance of the star Sirius (Sothis) โญ To line it up with the solar year, they invented a civil calendar by adding five intercalary days for a total of 365 days. Over time, though, people noticed the civil year was about one-fourth of a day shorter than the actual solar year, so it would precess relative to it over a 1460-year cycle called the Sothic cycle ๐Ÿ”„ So the ancient Egyptians and the Shang Chinese both ended up at the solar year being 365.25 days, which is within about 11 minutes of the present measured value. Then in 432 BC, about a century after the Chinese did it, the Greek astronomer Meton calculated 110 lunar months of 29 days and 125 lunar months of 30 days, for a total of 235 lunar months in 6940 solar days, or just over 19 years. That 19-year cycle, called the Metonic cycle, set the lunar month at 29.532 solar days, which is within about two minutes of the present measured value ๐ŸŽฏ

The Roman republican calendar was based on a lunar year, and by 50 BC it was eight weeks out of step with the solar year ๐Ÿ˜ฌ Julius Caesar invited the Alexandrian astronomer Sosigenes to redesign it, which led to the Julian calendar being adopted in 46 BC. This calendar is based on a year of 365 days, with an intercalary day inserted every four years ๐Ÿ‘‘ Unfortunately, for the first 36 years an intercalary day was mistakenly inserted every three years instead of every four. That meant 12 intercalary days instead of nine, and a series of corrections that wasn't finished until 8 AD ๐Ÿ™ˆ

The seven-day Sumerian week was only introduced in the fourth century AD by Emperor Constantine I ๐Ÿ—“๏ธ During the Roman era, a 15-year census cycle called the Indiction cycle was set up for taxation purposes. The sequence of day-names for consecutive occurrences of a particular day of the year doesn't repeat for 28 years, which is called the solar cycle. So the least common multiple of the 28-year solar cycle, the 19-year Metonic cycle and the 15-year Indiction cycle gives a grand 7980-year supercycle called the Julian Era, which started in 4713 BC ๐ŸŒŒ A particular combination of day of the week, day of the year, phase of the Moon and round of the census will recur beginning in 3268 AD. Wild, right? ๐Ÿฆ‹

By 1545 the discrepancy in the Julian year relative to the solar year had built up to ten days ๐Ÿ˜ต In 1582, following suggestions from the astronomers Christopher Clavius and Luigi Lilio, Pope Gregory XIII issued a papal bull that decreed, among other things, that the solar year would be 365.2422 days. To get closer to that new value, only centennial years divisible by 400 would be leap years, while the other centennial years wouldn't be. That makes the actual value 365.2425, which is within about 26 seconds of the current measured value โœจ Since the beginning of the Common Era and before 1990, 474 intercalary days were inserted into the Julian calendar, but 14 of those were removed in the Gregorian calendar. The Gregorian calendar is used throughout most of the world today, but some countries didn't adopt it until early in the twentieth century ๐ŸŒ

Okay, this is the bit I'm obsessed with ๐Ÿฅน While it's a fascinating field for time historians, the narrative above is conclusive evidence that converting calendar dates of significant events and assigning NTP timestamps to them is approximate at best ๐Ÿ“… In principle, reliable dating of such events only requires an accurate count of the days relative to some globally alarming event, like a comet passage or a supernova explosion. But only historically persistent and politically stable societies, like the ancient Chinese and Egyptians and especially the classic Maya, had the means and the will to do it ๐Ÿ™Œ

The Modified Julian Day System

In order to measure the span of the universe or the decay of the proton, it's necessary to have a standard day-numbering plan ๐ŸŒ  So the International Astronomical Union adopted the standard second and the Julian Day Number (JDN) to date cosmological events and related phenomena ๐Ÿ’ซ A standard day is 86,400 standard seconds, where time is expressed as a fraction of the whole day, and a standard year is 365.25 standard days ๐Ÿซถ

In the scheme devised in 1583 by the French scholar Joseph Justus Scaliger, named after his father, Julius Caesar Scaliger, JDN 0.0 corresponds to 12h (noon) on the first day of the Julian Era, which is 1 January 4713 BC ๐Ÿ‘‘ Years before the Common Era (BC) are counted using the Julian calendar, while years of the Common Era (AD) are counted using the Gregorian calendar. Since 1 January 1 AD in

the Gregorian calendar corresponds to 3 January 1 in the Julian calendar [DER90], and JDN 1,721,426.0 is 12h on the first day of the Common Era, 1 January 1 AD ๐ŸŽ€ that's the main character moment of the whole timeline ๐Ÿ‘‘. The Modified Julian Date (MJD) is sometimes used for dates near our own era because it's way more conventional and has fewer digits ๐Ÿ’…, and it's defined as MJD = JD <196> 2,400,000.5. By the convention that our century kicked off at 0h on 1 January 1900 (and the tropical year was already 12h old at that point ๐Ÿ˜ญ), that instant is MJD 15,020.0. So the Julian timescale ticks in standard (atomic) 365.25-day centuries ๐Ÿฆ‹. It was set to a given value at the approximate epoch of a cosmic event that basically synchronized the whole human community: the origin of the Common Era โœจ.

Determination of Frequency

Okay so for a long time the biggest use of time and frequency info was worldwide navigation and space science ๐Ÿงญ, which depend on astronomical observations of the Sun, Moon and stars [JOR85] ๐ŸŒ™. Sidereal time is based on the transit of stars across the celestial meridian of an observer โญ. The mean sidereal day is 23 hours, 56 minutes and 4.09 seconds, but it varies about <F128M>๏ฟฝ<F255D>30 ms throughout the year because of polar wandering and orbit variations ๐ŸŒ€. Ephemeris time is based on tables that let you figure out a standard time interval, like the tropical year (one complete revolution of the Earth around the Sun), by watching the Sun, Moon and planets ๐Ÿช. In 1958 the standard second got defined as 1/31,556,925.9747 of the tropical year that began this century ๐Ÿ“…. On that scale the tropical year is 365.2421987 days and the lunar month (one complete revolution of the Moon around the Earth) is 29.53059 days ๐ŸŒ•. Still, the actual tropical year can only be determined to an accuracy of about 50 ms, and it's been increasing by about 5.3 ms per year ๐Ÿ™ˆ.

Of the three heavenly oscillators that ancient mariners and astronomers could readily see (the Earth rotating on its axis, the Earth revolving around the Sun, and the Moon revolving around the Earth), none of them have the intrinsic stability, relative to modern technology, to serve as a standard reference oscillator ๐Ÿ’€. So in 1967 the standard second was redefined as <169>9,192,631,770 periods of the radiation corresponding to the transition between the two hyperfine levels of the ground state of the cesium-133 atom <170> ๐Ÿ”ฌโœจ. Since 1972 the time and frequency standards of the world have been based on International Atomic Time (TAI) ๐ŸŒ, which is defined and maintained using multiple cesium-beam oscillators to an accuracy of a few parts in 1013, or better than a microsecond per day ๐Ÿซถ. Note that while this is an extraordinarily precise timescale, it doesn't necessarily agree with conventional solar time, and it might not even be absolutely uniform, unless subtle atomic conspiracies can be ruled out ๐Ÿ•ต๏ธโ€โ™€๏ธ๐Ÿ‘€.

Determination of Time and Leap Seconds

The International Bureau of Weights and Measures (IBWM) uses astronomical observations provided by the U.S. Naval Observatory and other observatories to determine UTC ๐Ÿ”ญ. Starting from apparent mean solar time as observed, the UT0 timescale is determined using corrections for Earth orbit and inclination (the Equation of Time, the thing sundials use) ๐ŸŒž, the UT1 (navigator's) timescale is made by adding corrections for polar migration, and the UT2 timescale adds corrections for known periodicity variations ๐Ÿง . Standard frequencies are based on TAI, but conventional civil time is based on UT1, which is presently slowing relative to TAI by a fraction of a second per year ๐Ÿข. When the magnitude of the correction approaches 0.7 second, a leap second gets inserted or deleted in the TAI timescale on the last day of June or December ๐Ÿ—“๏ธโœจ.

For the most precise coordination and timestamping of events since 1972, you need to know when leap seconds are implemented in UTC and how the seconds are numbered ๐ŸŽฏ. As specified in CCIR Report 517, which is reproduced in [BLA74], a leap second is inserted following second 23:59:59 on the last day of June or December and becomes second 23:59:60 of that day โฐ. A leap second would be deleted by omitting second 23:59:59 on one of these days, although that has never happened ๐Ÿ™ƒ. Leap seconds were inserted prior to 1 January 1991 on the occasions listed in Table 8<$&tab8> (courtesy U.S. Naval Observatory) ๐Ÿ“‹. Published IBWM corrections are not just leap seconds, which cause step discontinuities relative to TAI. They also include 100-ms UT1 adjustments called DUT1, which give extra accuracy for navigation and space science ๐Ÿš€.

Note that the NTP time column actually shows the epoch following the last second of the day given in the UTC date and MJD columns (except for the first line). That's the precise epoch of insertion ๐ŸŽ€. The offset column shows the cumulative seconds offset between the uncoordinated (Julian) timescale and the UTC timescale. In other words, it's the number of seconds to add to the Julian clock to keep nominal agreement with the UTC clock ๐Ÿงฎ. Finally, the epoch of insertion is relative to the timescale immediately before that epoch. For example, the epoch of the 31 December 90 insertion is determined on the timescale in effect after the 31 December 1990 insertion. So the actual insertion relative to the Julian clock is fourteen seconds later than the apparent time on the UTC timescale ๐Ÿซ .

The UTC timescale ticks in standard (atomic) seconds and was set to the value 0h MJD 41,317.0 at the epoch determined by astronomical observation to be 0h on 1 January 1972 according to the Gregorian calendar. That's the inaugural tick of the UTC Era ๐ŸŒŸ. Honestly, the inaugural tick that synchronized the cosmic oscillators, Julian clock, UTC clock and Gregorian calendar forevermore was about ten seconds off from the civil clock then in use ๐Ÿ˜ญ, and the GPS clock is ahead of the UTC clock by six seconds in late 1990 ๐Ÿ›ฐ๏ธ. Since then, the UTC clock has marched backward relative to the Julian timescale by exactly one second on scheduled occasions, at monumental epoches stored in the institutional memory of our civilization ๐Ÿ›๏ธ. Also, leap-second adjustments affect the number of seconds per day, and therefore the number of seconds per year ๐Ÿ“†. Apparently, if we ever choose to worry about it, the UTC clock, Julian clock and various cosmic clocks will inexorably drift apart over time, until some future papal bull rationalizes it ๐Ÿ“œ๐Ÿ’….

The NTP Timescale and Reckoning with UTC

The NTP timescale is based on the UTC timescale, but it's not necessarily always coincident with it ๐Ÿซถ. At 0h on 1 January 1972 (MJD 41,317.0), the first tick of the UTC Era, the NTP clock was set to 2,272,060,800, which is the number of standard seconds since 0h on 1 January 1900 (MJD 15,020.0) ๐Ÿง. Leap seconds inserted in UTC, and later into NTP, don't affect the UTC or NTP oscillator. They only affect the conversion to conventional civil UTC time ๐Ÿ•ฐ๏ธ. But the only institutional memory NTP has is the UTC timecode broadcast services, so the NTP timescale is effectively reset to UTC every time a timecode is received ๐Ÿ”„. That means when a leap second is inserted in UTC and then in NTP, knowledge of all previous leap seconds is lost ๐Ÿซฃ.

Another way to put it is that there are as many NTP timescales as there are historic leap seconds ๐Ÿ’ซ. In effect, a new timescale is established after each new leap second, so all previous leap seconds, not to mention the apparent origin of the timescale itself, lurch backward one second each time a new timescale gets established ๐Ÿ˜ตโ€๐Ÿ’ซ. If a clock synchronized to NTP in 1990 was used to establish the UTC epoch of an event that happened in early 1972 without correction, the event would appear fifteen seconds late relative to UTC ๐Ÿ™ˆ. However, NTP primary time servers resolve the epoch using the broadcast timecode, so the NTP clock is set to the broadcast value on the current timescale โœ…. So for the most precise determination of epoch relative to the historic UTC clock, the user must subtract the offsets shown in Table 8 at the relative epoches shown from the apparent NTP epoch ๐Ÿง . This is a feature of almost all present-day time-distribution mechanisms ๐ŸŽ€.

The chronometry involved can be illustrated with the help of Figure 8, which shows the details of seconds numbering just before, during and after the last scheduled leap insertion at 23:59:59 on 31 December 1989 ๐Ÿ“ธ. Notice that the NTP leap bits are set on the day before insertion, as shown by the <169>+<170> symbols on the figure ๐Ÿ‘€. Since this makes the day one second longer than usual, the NTP day rollover won't occur until the end of the first occurrence of second 800 โณ. The UTC time conversion routines have to notice the apparent time and the leap bits, then handle the timescale conversions accordingly ๐Ÿงฉ. Right after the leap insertion, both timescales resume ticking the seconds as if the leap never happened โœจ. The chronometric correspondence between UTC and NTP continues, but NTP has forgotten all past leap insertions ๐Ÿ’€. So in NTP chronometric determination of UTC time intervals spanning leap seconds, there will be errors, unless the exact times of insertion are known ๐Ÿ™ƒ.

It's possible that individual systems use internal data formats other than the NTP timestamp format, which is represented in seconds to a precision of about 200 picoseconds ๐Ÿ”ฌ. However, there's a persuasive argument for a two-part representation: one part for whole days (MJD or some fixed offset from it) and the other for the seconds (or some scaled value, such as milliseconds) ๐Ÿ’–. This not only makes converting between NTP and conventional civil time easier, but also makes inserting leap seconds much easier ๐Ÿซถ. All that's required is to change the modulus of the seconds counter, which on overflow increments the day counter ๐Ÿง. This design ensures the continuity of the timescale is assured, even if outside synchronization is lost before, during or after a leap-second insertion ๐Ÿ›ก๏ธ. And since timestamp data are unaffected, synchronization is assured even when timestamp data are in flight at that instant and originated before or at that instant ๐Ÿฆ‹๐ŸŒˆ.


Appendix F. The NTP Clock-Combining Algorithm

Introduction

A common problem in synchronization subnets is systematic time-offset errors that come from asymmetric transmission paths, where the networks or transmission media in one direction are substantially different from the other ๐Ÿ˜ญ. The errors can range from microseconds on high-speed ring networks to large fractions of a second on satellite/landline paths ๐Ÿ›ฐ๏ธ. It's been found experimentally that these errors can be considerably reduced by combining the apparent offsets of a number of time servers to produce a more accurate working offset ๐Ÿ”ฅ. Below is a description of the combining method used in the NTP implementation for the Fuzzball [MIL88b] ๐Ÿงš. The method is similar to the one national standards laboratories use to build a synthetic laboratory timescale from an ensemble of cesium clocks [ALL74b] โญ. These procedures are optional and not required in a conforming NTP implementation ๐Ÿซถ.

In the following description, the stability of a clock is how well it maintains a constant frequency, the accuracy is how well its frequency and time compare with national standards, and the precision is how precisely these quantities can be maintained within a particular timekeeping system ๐Ÿค“. Unless indicated otherwise, the offset of two clocks is the time difference between them, while the skew is the frequency difference (first derivative of offset with time) between them. Real clocks show some variation in skew (second derivative of offset with time), which is called drift ๐ŸŒธ.

Determining Time and Frequency

Figure 9<$&fig9> shows the overall organization of the NTP time-server model ๐Ÿ—บ๏ธ. Timestamps exchanged with possibly many other subnet peers are used to determine individual roundtrip delays and clock offsets relative to each peer, as described in the NTP specification ๐Ÿ’…. As shown in the

So, as that figure shows, the computed delays and offsets go through the clock filter ๐Ÿงน to scrub out incidental timing noise. Then the clock-selection algorithm picks the most accurate and reliable subset ๐Ÿ’–. The offsets from that subset get combined (more on that below) and fed into the phase-locked loop (PLL) ๐Ÿ”. Inside the PLL, the filtering, selection and combining effects together produce one phase-correction term. The loop filter takes that term and steers the local clock, which acts like a voltage-controlled oscillator (VCO) โšก. The VCO gives the timing (phase) reference used to make every timestamp in all the calculations ๐ŸŽ€.

Clock Modelling ๐Ÿ•ฐ๏ธ

Okay, so the official International Standard (SI) definition of a time interval is based on the standard second: "the duration of 9,192,631,770 periods of the radiation corresponding to the transition between the two hyperfine levels of the ground state of the cesium-133 atom." ๐Ÿงช Let u be that standard unit of time interval, and let the standard unit of frequency be:

<$Ev~=~1 over u>

The epoch, written t, is the reading of a counter that runs at frequency v and started counting at some agreed initial epoch t0. That starting point sets the standard or absolute timescale ๐Ÿ“. For this analysis, the epoch of the standard timescale and the time shown by a clock are both treated as continuous โฑ๏ธ. In real life, time is measured against a clock built from an atomic oscillator plus a system of counter/dividers, and that combo defines a timescale tied to that particular oscillator. Standard time and frequency are then worked out from an ensemble of these timescales, using algorithms that combine them into a composite timescale that approximates the standard timescale ๐ŸŒธ.

The time shown by a clock at epoch t, relative to the standard timescale, looks like this:

<$ET(t)~=~1/2 D(t sub 0 )[t~-~t sub 0 ] sup 2~+~R(t sub 0 )[t~-~t sub 0
]~ +~T(t sub 0 )~+~x(t)> ,

Here D(t0) is the fractional frequency drift per unit time, R(t0) is the frequency, and T(t0) is the time at some previous epoch t0 ๐Ÿงฎ. In the usual stationary model, these can be assumed constant or changing slowly with epoch. The clock's randomness is captured by x(t), which is the random noise (jitter) relative to the standard timescale ๐ŸŽฒ. Usually the second-order term D(t0) gets ignored, and the noise term x(t) is modelled as a normal distribution with predictable spectral density or autocorrelation function ๐Ÿ’….

The probability density function of the time offset, p(t - T(t)), usually looks like a bell-shaped curve centred somewhere near zero ๐Ÿ””. The width and general shape of that curve come from x(t), which depends on the oscillator precision and jitter characteristics, as well as the measurement system and its transmission paths ๐Ÿ›ค๏ธ. Starting at epoch t0, the offset is set to zero. After that, the bell creeps to the left or right depending on the value of R(t0), and it speeds up depending on the value of D(t0) ๐ŸŽข.

Development of a Composite Timescale โณ

Now let's think about the time offsets of a bunch of real clocks connected by real networks ๐ŸŒ. If you display the offsets of all the clocks relative to the standard timescale, you get a whole system of bell-shaped curves slowly precessing relative to each other. Some sit further away from nominal zero than others ๐Ÿซ . The bells are usually scattered over the offset space, more or less close together, with some overlapping and some not ๐Ÿ“. The problem is estimating the true offset relative to the standard timescale from the system of offsets collected routinely between the clocks ๐Ÿ”.

A composite timescale can be determined from a sequence of offsets measured between the n clocks of an ensemble, at nominal intervals tau. Let R_i(t0) be the frequency and T_i(t0) be the time of the ith clock at epoch t0, relative to the standard timescale, and let the associated estimates be marked with a hat. Then an estimator for T_i, computed at t0 for epoch t0 + tau, is:

<$ET hat sub i ( t sub 0~+~ tau )~=~R hat sub i (t sub 0 ) tau ~+~T sub
i (t sub 0 )> ,

This neglects second-order terms โœ‚๏ธ. Now take a set of n independent time-offset measurements made between the clocks at epoch t0 + tau. The offset between clock i and clock j at that epoch is defined as:

<$ET sub ij (t sub 0~+~ tau )~==~T sub i (t sub 0~+~ tau )~-~T sub j (t
sub 0~+~ tau )> .

Note that <$ET sub ij~=~- T sub ji> and <$ET sub ii~=~0> ๐Ÿ’ซ. Let w_i(tau) be a previously determined weight factor for the ith clock, for the nominal interval tau. The basis for new estimates at epoch t0 + tau is:

<$ET sub j (t sub 0~+~tau )~=~sum from {i=1} to n w sub i ( tau )[ T hat
sub i (t sub 0~+~tau )~+~T sub ji (t sub 0~+~tau )].>

Put simply, the apparent time shown by the jth clock is a weighted average. You take the estimated time of each clock at epoch t0 + tau, add the time offset measured between the *j*th clock and that clock at the same epoch, and average the results with the weights ๐Ÿง.

A few examples help you get the vibe of how this algorithm behaves ๐Ÿฆ‹. If w_i(tau) is unity for the ith clock and zero for all the others, then the apparent time for each of the other clocks is just the estimated time for the ith clock. If w_i(tau) is zero for the ith clock, that clock can never affect any other clock, and its apparent time gets determined entirely from the other clocks ๐Ÿ˜ค. If w_i(tau) = 1/n for all i, the apparent time of the ith clock equals the average of the time estimates computed at t0, plus the average of the time offsets measured to all the other clocks ๐Ÿ“Š. Last one: in a system with two clocks and w_i(tau) = 1/2 for each, if the estimated time at epoch t0 + tau is fast by 1 s for one clock and slow by 1 s for the other, the apparent time for both clocks will coincide with the standard timescale ๐ŸŽฏ.

To set up the basis for the next interval tau, we have to update the frequency estimate and the weight factor. The average frequency assumed for the ith clock during the previous interval tau is just the difference between the times at the beginning and end of the interval, divided by tau ๐Ÿง . A good estimator for the frequency has been found to be the exponential average of these differences, given by:

<$ER hat sub i (t sub 0~+~tau )~=~R hat sub i (t sub 0 )~+~alpha sub i [
R hat sub i (t sub 0 )~-~{T sub i (t sub 0~+~tau )~-~T sub i (t sub 0 )}
over tau ]> ,

Here alpha_i is an experimentally determined weight factor, and it depends on the estimated frequency error of the ith clock ๐Ÿงช. To calculate the weight factor w_i(tau), you first need the expected error epsilon_i(tau) for each clock. In what follows, braces mean absolute value, and brackets mean the infinite time average. In practice, the infinite averages get computed as exponential time averages ๐Ÿ•ฐ๏ธ. The estimate of the magnitude of the unbiased error of the ith clock, accumulated over the nominal interval tau, is:

<$Eepsilon sub i ( tau )~=~| T hat sub i ( t sub 0~+~tau )~-~T sub i ( t
sub 0~+~tau ) |~+~{0.8~<<~epsilon sub e sup 2 ( tau )~>> } over sqrt {
<<~epsilon sub i sup 2 ( tau )~>> }> ,

In that formula, epsilon_i(tau) and epsilon_e(tau) are the accumulated error of the *i*th clock and of the entire clock ensemble, respectively ๐Ÿ’–. The accumulated error of the entire ensemble is:

<$E<<~epsilon sub e sup 2 ( tau )~>>~=~left [ sum from i=1 to n~1 over {
<<~epsilon sub i sup 2 ( tau )~>> } right ] sup {~-1}>.

And finally, the weight factor for the *i*th clock is calculated as:

<$Ew sub i ( tau )~=~ { <<~epsilon sub e sup 2 ( tau )~>> } over {
<<~epsilon sub i sup 2 ( tau )~>> }> .

Once all the estimators and weight factors have been updated, the origin of the estimation interval shifts. The new value of t0 becomes the old value of t0 + tau ๐Ÿ”„.

Not part of the calculations above, but still useful: it's worth estimating the frequency error, because the ensemble clocks can be located some distance from each other, and they can get isolated for a while because of network failures ๐ŸŒง๏ธ. The frequency-offset error in R_i is equivalent to the fractional frequency y_i:

<$Ey sub i~=~{ nu sub i~-~nu sub I } over nu sub I>

This is measured between the ith timescale and the standard timescale I. Let's temporarily drop the subscript i for clarity ๐Ÿซถ. Consider a sequence of N independent frequency-offset samples, <$Ey(j)~ (j~=~1,~2,~... ,~N)>, where the interval between samples is uniform and equal to T. Let tau be the nominal interval over which these samples are averaged. The Allan variance <$Esigma sub y sup 2 ( N,~T,~tau )> [ALL74a] is defined as:

<$E<< sigma sub y sup 2 ( N,~T,~tau )~>>~=~<< ~ 1 over { N~-~1 }~ left [
sum from j=1 to N~y (j) sup 2~-~1 over N~left ( sum from j=1 to N~y(j)
right ) sup 2 right ]~>>> ,

A particularly useful version sets N = 2 and T = tau:

<$E<< sigma sub y sup 2 (N~=~2,~T~=~tau ,~tau )>>~==~sigma sub y sup 2 (
tau )~=~<< {[y(j~+~1)~-~y(j)] sup 2 } over 2 >>>

so that

<$Esigma sub y sup 2 ( tau )~=~1 over {2(N~-~1)}sum from { j = 1 } to
{n-1 }~[y(j~+~1)~-~y(j)] sup 2> .

๐ŸŽ€ The Allan variance has been used when estimating errors in ensembles of cesium clocks, but for NTP its use is limited by the computation and storage burden. As the next section explains, you can estimate errors with some degree of confidence by using normal byproducts of the NTP processing algorithms ๐ŸŒˆ.

Application to NTP ๐Ÿ›ฐ๏ธ

The NTP clock model is somewhat less complex than the general model described above ๐Ÿ˜Œ. For instance, at the present level of development, it's not necessary to estimate the time and frequency of all peer clocks separately. Only the time and frequency of the local clock matter. If the timekeeping reference is the local clock itself, then the offsets available in the peer.offset peer variables can be used directly for the <$ET sub ij> quantities above ๐Ÿ™Œ. On top of that, the NTP local-clock model includes a type-II phase-locked loop, which reliably estimates frequency errors by itself and corrects accordingly ๐Ÿ”. So the requirement for estimating frequency is entirely eliminated โœจ.

There's still the problem of how to get a robust and easily computable error estimate, <$Eepsilon sub i>. The method above is analytically justified, but it's super difficult to implement ๐Ÿฅฒ. Good news, though: as a byproduct of the NTP clock-filter algorithm, a useful error estimate is available in the form of the dispersion ๐ŸŽ. As the NTP specification describes, the dispersion includes the absolute value of the weighted average of the offsets between the chosen offset sample and the n - 1 other samples retained for selection ๐Ÿงฉ. This estimator's effectiveness was compared with the one above, using simulation on observed timekeeping data, and it gave quite acceptable results ๐Ÿ’….

The NTP clock-combining algorithm can be implemented with only minor modifications to the algorithms described in the NTP specification ๐Ÿฐ. The NTP specification has mostly avoided general-purpose multiply/divide routines, which is a big win, but there seems to be no way to avoid them in the clock-combining algorithm ๐Ÿ˜ญ. For best performance, though, the local-clock algorithm described elsewhere in this document should be implemented as well ๐Ÿ”ง. The reason is that the combining algorithms cause a modest increase in phase noise, and the revised local-clock algorithm is designed to suppress that noise ๐Ÿซง.

Clock-Combining Procedure ๐Ÿ‘‘

The result of the NTP clock-selection procedure is a set of survivors

(there must be at least one ๐Ÿซถ) that represent truechimers, aka correct clocks โœจ. When clock combining isn't implemented, one of these peers, picked as the most likely candidate, becomes the synchronization source, and its computed offset becomes the final clock correction. After that, the system variables get adjusted as described in the NTP clock-update procedure. When clock combining is implemented, all of that stays the same, except the final clock correction gets computed by the clock-combining procedure ๐Ÿ’….

The clock-combining procedure is called from the clock-select procedure. It builds the final clock correction <$ETHETA> from the variables of all surviving peers. The estimated error that the earlier algorithms need is based on the synchronization distance <$ELAMBDA>, which the distance procedure computes as defined in the NTP specification. The reciprocal of <$ELAMBDA> is the weight of each clock-offset contribution to the final clock correction. Here's the pseudo-code ๐Ÿ‘€:

begin clock-combining procedure
        <$Etemp1~<<-~0>;
        <$Etemp2~<<-~0>;
        for (each peer remaining on the candidate list)         /* scan
all survivors */
                <$ELAMBDA~<<-~roman distance (peer)>;
                <$Etemp~<<-~1 over roman
{peer.stratum~times~NTP.MAXDISPERSE~+~LAMBDA }>;
                <$Etemp1~<<-~temp1~+~temp>;             /* update weight
and offset */
                <$Etemp2~<<-~temp2~+~temp~times~roman peer.offset>;
                endif;
        <$ETHETA~<<-~temp2 over temp1>;
/* compute final correction */
        end clock-combining procedure;

The value <$ETHETA> is the final clock correction that the local-clock procedure uses to adjust the clock ๐ŸŽ€.

Appendix G. Computer Clock Modelling and Analysis ๐Ÿงš

A computer clock has a reference oscillator, stabilized by a quartz crystal or some other means, such as the power grid โšก. Usually the clock also has a prescaler, which divides the oscillator frequency down to a standard value like 1 MHz or 100 Hz, plus a counter, built in hardware, software, or a mix of both, that the processor can read. For systems meant to be synced to an external source of standard time, there must be some means to correct the phase and frequency with occasional vernier adjustments from the timekeeping protocol. Special care is needed in every timekeeping design to make sure the clock indications are always monotonically increasing, which means system time never <169>runs backwards.<170> ๐Ÿ˜ญ

Computer Clock Models

The simplest computer clock is a hardware latch that gets set when a hardware counter or prescaler overflows, causing a processor interrupt, or tick. The processor acknowledges it, which resets the latch, and then increments the value of a software clock counter. The phase of the clock gets adjusted by adding periodic corrections to that counter whenever needed. The frequency can be adjusted by changing the increment itself, so the clock runs faster or slower. The precision of this simple model is limited to the tick interval, usually around 10 ms, although on some systems the tick interval can be changed using a kernel variable ๐Ÿ“.

This software model needs a processor interrupt on every tick, which can cause real overhead if the tick interval is small, say less than 1 ms on newer RISC processors. So, to get timekeeping precision below 1 ms, you need some kind of hardware assist. A straightforward design is a voltage-controlled oscillator (VCO), where the frequency is controlled by a buffered digital/analog converter (DAC). Assuming the VCO tolerance is 10-4, or 100 parts-per-million (ppm) (a reasonable value for inexpensive crystals), and the required precision is 100 <$Emu roman s> (a reasonable goal for a RISC processor), the DAC must include at least ten bits ๐Ÿ’œ.

A design sketch of a computer clock built entirely from hardware logic components is shown in Figure 10a<$&fig10>. The clock gets read by first pulsing the read signal, which latches the current value of the clock counter. Then the contents of the clock-counter latch get added to a 64-bit clock-offset variable that lives in processor memory. The clock phase is adjusted by adding a correction to the clock-offset variable, while the clock frequency is adjusted by loading a correction into the DAC latch. In principle this model can be adapted to any precision by changing the number of bits in the prescaler or clock counter, or by changing the VCO frequency. Still, it doesn't really make sense to go much below the minimum interrupt latency, which is in the low microseconds on a modern RISC processor ๐Ÿคทโ€โ™€๏ธ.

If it isn't possible to vary the oscillator frequency, which might be the case if the oscillator is an external frequency standard, a design like the one in Figure 10b may be used. It has a fixed-frequency oscillator and a prescaler that includes a dual-modulus swallow counter. That counter can run in either divide-by-10 or divide-by-11 mode, controlled by a pulse from a programmable divider (PD). The PD is loaded with a value representing the frequency offset. Every time the divider overflows, it produces a pulse that switches the swallow counter from divide-by-10 mode to divide-by-11 mode and then back again. That effectively <169>swallows<170>, or deletes, a single pulse of the prescaler pulse train ๐Ÿฆ‹.

The prescaler's pulse train is controlled precisely over a small range by the contents of the PD. If the PD is programmed to emit pulses at a low rate, relatively few pulses get swallowed per second, and the counted frequency sits near the upper limit of its range. If it's programmed to emit pulses at a high rate, relatively many pulses get swallowed, and the counted frequency sits near the lower limit. As long as there's some freedom in choosing the oscillator frequency and prescaler ratios, this design can compensate for a wide range of oscillator frequency tolerances ๐ŸŒˆ.

In all of the designs above, it's necessary to limit how much adjustment goes into any single step, so the system clock indications stay monotonically increasing. With the software clock model, this is guaranteed as long as the increment is never negative. When the magnitude of a phase adjustment is bigger than the tick interval (as corrected for the frequency adjustment), the adjustments have to be spread over multiple tick intervals. This strategy amounts to a deliberate frequency offset that gets sustained for an interval equal to the total number of ticks required. Honestly, it's a feature of the Unix clock model discussed below ๐Ÿซถ.

In the hardware clock models the same considerations apply. However, in these designs the tick interval amounts to a single pulse at the prescaler output, which may be on the order of 1 ms. To avoid decreasing the indicated time when a negative phase correction occurs, you have to avoid modifying the clock-offset variable in processor memory and confine all adjustments to the VCO or prescaler. So all phase adjustments must be done with programmed frequency adjustments, much like the software clock model described earlier ๐Ÿ’….

It's interesting to imagine a processor assist that could provide all of the functions above in a compact, general-purpose hardware interface ๐Ÿ”ฅ. The interface might be a multifunction timer chip like the AMD 9513A, which has five 16-bit counters, each with programmable load and hold registers, plus an onboard crystal oscillator, prescaler, and control circuitry. A 48-bit hardware clock counter would use three of the 16-bit counters, the fourth would act as the swallow counter, and the fifth would serve as the programmable divider. Add a programmable-array logic device and an architecture-specific host interface, and this compact design could provide all the functions needed for a complete timekeeping system ๐Ÿ‘‘.

The Fuzzball Clock Model

The Fuzzball clock model mixes hardware and software to deliver precision timing with minimal software and processor overhead โœจ. The model includes an oscillator, a prescaler, and a hardware counter. However, the oscillator frequency stays constant, and the hardware counter only produces a fraction of the total number of bits the clock counter needs. A typical design uses a 64-bit software clock counter and a 16-bit hardware counter that counts the prescaler output. A hardware-counter overflow causes the processor to increment the software counter at the bit corresponding to the frequency <$E2 sup N f sub p>, where N is the number of bits in the hardware counter and fp is the counted frequency at the prescaler output. To read the clock counter, the processor first generates a read pulse, which latches the hardware counter, and then adds its contents, suitably aligned, to the software counter ๐Ÿงš.

The Fuzzball clock can be corrected in phase by adding a (signed) adjustment to the software clock counter. In practice, this only happens when the local time is substantially different from the time the clock indicates, and it may violate the monotonicity requirement ๐Ÿ˜ฌ. Vernier phase adjustments determined during normal system operation must be limited to no more than the period of the counted frequency, which is 1 kHz for LSI-11 Fuzzballs. In the Fuzzball model these adjustments happen at intervals of 4 s, called the adjustment interval, which gives a maximum frequency adjustment range of 250 ppm. The adjustment opportunities are created using the interval-timer facility, a feature of most operating systems that is independent of the time-of-day clock. However, if the counted frequency goes up from 1 kHz to 1 MHz for better precision, the adjustment frequency has to go up to 250 Hz, which really increases processor overhead ๐Ÿ™ˆ. A modified design suitable for high-precision clocks is presented in the next section ๐Ÿ’–.

In some applications of the Fuzzball model, an external pulse-per-second (pps) signal is available from a reference source such as a cesium clock or GPS receiver. A signal like that is generally much more accurate than the serial character string from a radio timecode receiver, typically in the low nanoseconds. In the Fuzzball model, this signal goes through an interface that produces a hardware interrupt coincident with the arrival of the pps pulse. The processor then reads the clock counter and computes the residual modulo 1 s of the clock counter. That residual is the local-clock error relative to the pps signal ๐ŸŒธ.

Assuming the seconds numbering of the clock counter was determined by a reliable source, such as a timecode receiver, the offset within the second is determined by the residual computed above. In the NTP local-clock model, the timecode receiver or NTP establishes the time to within <F128M>๏ฟฝ<F255D>128 ms, called the aperture, which guarantees the seconds numbering to within the second. Then the pps residual can be used directly to correct the oscillator, since the offset must be less than the aperture for a correctly operating timecode receiver and pps signal ๐ŸŽ€.

The technique above has an inherent error equal to the latency of the interrupt system, which on modern RISC processors is in the low tens of microseconds. Accuracy can be improved by latching the hardware time-of-day counter directly with the pps pulse, and then reading the counter in the usual way. This needs some extra circuitry to prioritize the pps signal over the pulse the program generates to latch the counter โญ.

The Unix Clock Model

The Unix 4.3bsd clock model is based on two system calls, settimeofday

and adjtime, together with two kernel variables, tick and tickadj โœจ settimeofday literally resets the kernel clock to whatever value you hand it, no warning, no vibes check ๐Ÿ’€. adjtime is the gentler one. It slews the kernel clock toward a new value that equals the present time of day plus the signed argument you pass in. To really get how the Unix clock behaves under the Fuzzball clock model we just went over, we need to dig into how adjtime works ๐Ÿ”

The Unix clock model assumes an interrupt from an onboard frequency source, like the clock counter and prescaler we covered earlier, delivering a pulse train in the 100-Hz range. In principle the power grid frequency could be used too, though it's way less stable than a crystal oscillator ๐Ÿ™ƒ Each interrupt adds an increment called tick to the clock counter. That increment is picked so the clock counter, plus an initial offset set by settimeofday, equals the time of day in microseconds ๐Ÿ•ฐ๏ธ

The Unix clock can run at three different rates. One matches tick, which comes from the intrinsic frequency of whichever oscillator is the clock source. The other two are tick + tickadj and tick โˆ’ tickadj. Normally the tick rate is in use. But when adjtime gets called, its argument delta is used to work out an interval:

DELTA t = delta tick over tickadj

During that interval, one of the other two rates runs, depending on the sign of delta. The point is to slew the clock to its new value at a small, constant rate instead of applying the whole adjustment at once, which could set the clock backward ๐Ÿ˜ฌ With common values of tick = 10 ms and tickadj = 5 ยตs, the maximum frequency adjustment range works out to:

ยฑtickadj over tick = ยฑ{5 x 10^-6} over {10^-2} = ยฑ500 ppm

Some workstations, like SPARCstations with really poor component tolerances, might need even bigger ranges ๐Ÿคฏ

When you need precisions of about 1 ms or better, the Fuzzball clock model can be adapted to the Unix model with software simulation, as described in Section 5 of the NTP specification, calling adjtime at each adjustment interval ๐Ÿงƒ When you need precisions way better than that, the hardware microsecond clock some workstations have can be used, along with certain refinements to the Fuzzball and Unix clock models. The design below is built for a maximum oscillator frequency tolerance of 100 ppm (.01%), which a fairly cheap quartz crystal oscillator can hit, and it's easily scalable to other tolerances too ๐Ÿ’–

The clock model needs to be able to slew the clock frequency across the range ยฑ100 ppm, with an intrinsic oscillator frequency error as big as ยฑ100 ppm. Figure 11 shows the timing relationships at the extremes of this requirements envelope ๐Ÿ“ˆ Start from an assumed offset of nominal zero, with an assumed error of +100 ppm at time 0 s. The line AC shows how the uncorrected offset grows over time. Let ฯƒ be the adjustment interval, and let a be the interval AB, in seconds. Let r be the slew, the rate at which corrections get introduced, in ppm. For an accuracy specification of 100 ยตs, we get:

sigma <= {100 us} over {100 ppm} + {100 us} over {(r - 100) ppm} = r over {r - 100}

The line AE is the extreme case where the clock is steered at โˆ’100 ppm. Since the slew has to be complete by the end of the adjustment interval:

a <= {(r - 200) sigma} over r

These relationships only hold when r > 200 ppm and ฯƒ < 2 s. Using r = 300 ppm for convenience gives ฯƒ = 1.5 s and a <= 0.5 s. For the Unix clock model with tick = 10 ms, this gives a tickadj value of 3 ยตs ๐ŸŽ€

One assumption in the Unix clock model is that the adjustment period computed in an adjtime call must finish before the next call is made. If it doesn't, an error message goes to the system log ๐Ÿšจ But to correct for the intrinsic frequency offset of the clock oscillator, the NTP clock model needs adjtime called at regular adjustment intervals of ฯƒ seconds. Using the algorithms described here and the architecture constants in the NTP specification, these adjustments will always complete โœ…


Mathematical Model of the NTP Logical Clock

The NTP logical clock can be represented by the feedback-control model shown in Figure 12 ๐Ÿ” The model has an adaptive-parameter phase-lock loop (PLL), which keeps adjusting the phase and frequency of an oscillator to compensate for its intrinsic jitter, wander and drift. A mathematical analysis of this model, developed along the lines of [SMI86], is presented in the following sections, along with a design example that's useful for implementation guidance in operating-system environments like Unix and Fuzzball. Table 9 summarizes the quantities usually treated as variables in the model. By convention, v is used for internal loop variables, ฮธ for phase, ฯ‰ for frequency and ฯ„ for time. Table 10 summarizes the quantities usually fixed as constants in the model. These are all expressed as a power of two to keep the implementation simple ๐Ÿ“

In Figure 12, ฮธ_r is the phase of the reference signal and ฮธ_o is the phase of the voltage-controlled oscillator (VCO). The phase detector (PD) produces a voltage v_d representing the phase difference ฮธ_r โˆ’ ฮธ_o. The clock filter works like a tapped delay line, with the output v_s taken at the tap picked by the clock-filter algorithm described in the NTP specification. The loop filter, given by the equations below, produces a VCO correction voltage v_c, which controls the oscillator frequency and therefore the phase ฮธ_o ๐ŸŒˆ

The PLL's behavior is completely determined by its open-loop Laplace transfer function G(s) in the s domain. Since both frequency and phase corrections are needed, a good design is a type-II PLL, defined by:

G(s) = {omega_c^2} over {tau^2 s^2} ( 1 + {tau s} over omega_z )

Here ฯ‰_c is the crossover frequency (also called loop gain), ฯ‰_z is the corner frequency (required for loop stability), and ฯ„ sets the PLL time constant and therefore the bandwidth ๐Ÿ’œ This is a first-order function, and some improvement in phase noise might come from a higher-order one, but in practice that improvement gets lost to the effects of the clock-filter delay, as explained below ๐Ÿ™„

The open-loop transfer function G(s) is built by breaking the loop at point a in Figure 12 and computing the ratio of the output phase ฮธ_o(s) to the reference phase ฮธ_r(s). It's the product of the individual transfer functions for the phase detector, clock filter, loop filter and VCO. The phase detector delivers v_d(t) = ฮธ_r(t), so its transfer function is simply F_d(s) = 1, expressed in V/rad. The VCO delivers a frequency change ฮ”ฯ‰ = dฮธ_o(t)/dt = ฮฑ v_c(t), where ฮฑ is the VCO gain in rad/V-sec, and ฮธ_o(t) = ฮฑ โˆซ v_c(t) dt. Its transfer function is the Laplace transform of the integral, F_o(s) = ฮฑ/s, expressed in rad/V. The clock filter adds a stochastic delay due to the clock-filter algorithm, but for now that delay is assumed to be a constant T, so its transfer function is the Laplace transform of the delay, F_s(s) = e^{โˆ’Ts}. Let F(s) be the transfer function of the loop filter, which hasn't been determined yet. G(s) is the product of these four transfer functions:

G(s) = {omega_c^2} over {tau^2 s^2} ( 1 + {tau s} over omega_z ) = F_d(s) F_s(s) F(s) F_o(s) = 1 e^{โˆ’Ts} F(s) alpha/s

For the moment, assume the product Ts is small, so that e^{โˆ’Ts} โ‰ˆ 1. Making these substitutions:

omega_c^2 = alpha / K_f        and        omega_z = K_g / K_f

and rearranging gives:

F(s) = 1 / (K_g tau) + 1 / (K_f tau^2 s)

This is a constant term plus an integrating term scaled by the PLL time constant ฯ„. The form is handy for implementing a sampled-data system, which we'll get to later ๐Ÿง 

With the parameter values in Table 10, the Bode plot of the open-loop transfer function G(s) has a โˆ’12 dB/octave line that crosses the 0-dB baseline at ฯ‰_c = 2^โˆ’12 rad/s, plus a +6 dB/octave line at the corner frequency ฯ‰_z = 2^โˆ’14 rad/s. The damping factor ฮถ = ฯ‰_c/(2ฯ‰_z) = 2 suggests the PLL will be stable, with a large phase margin and low overshoot ๐Ÿ˜Œ However, if the clock-filter delay T isn't small compared to the loop delay, which is approximately 1/ฯ‰_c, the analysis above becomes unreliable and the loop can go unstable ๐Ÿ˜ฑ With the values worked out above, T is usually small enough to ignore.

Assuming the output is taken at v_s, the closed-loop transfer function H(s) is:

H(s) = v_s(s) / ฮธ_r(s) = F_d(s) e^{โˆ’Ts} / (1 + G(s))

If only the relative response matters and the clock-filter delay can be neglected, H(s) can be written as:

H(s) = 1 / (1 + G(s)) = s^2 / (s^2 + (omega_c^2 / (omega_z tau)) s + omega_c^2 / tau^2)

For some input function I(s), the output function I(s)H(s) can be inverted to find the time response. Using a unit-step input I(s) = 1/s and the values worked out above, this gives a PLL risetime of about 52 minutes, a maximum overshoot of about 4.8 percent at about 1.7 hours, and a settling time to within one percent of the initial offset in about 8.7 hours โฑ๏ธ

Parameter Management

A really important feature of the NTP PLL design is that it can adapt its behavior to match how stable the local oscillator is and what the network conditions are like ๐Ÿฆ‹ This is done using the ฮฑ and ฯ„ parameters shown in Table 10. The mechanisms for doing this are described in the following sections.

Adjusting VCO Gain (ฮฑ)

The ฮฑ parameter is set by the maximum frequency tolerance of the local oscillator and the maximum jitter requirements of the timekeeping system. It's usually an architecture constant that stays fixed during system operation ๐Ÿ”’ In the implementation model described below, the reciprocal of ฮฑ, called the adjustment interval ฯƒ, sets the time between corrections of the local clock, and that's how ฮฑ's value comes out. The value of ฯƒ can be determined by the following procedure.

The maximum frequency tolerance for board-mounted, uncompensated quartz-crystal oscillators is probably in the range of 10^โˆ’4 (100 ppm). Lots of Internet timekeeping systems can tolerate jitter at least on the order of the intrinsic local-clock resolution, called precision in the NTP specification, which is commonly somewhere from one to 20 ms. Assuming 10^โˆ’3 s peak-to-peak as the most demanding case, the interval between clock corrections must be no more than:

sigma = 10^โˆ’3 over {2 x 10^โˆ’4} = 5 sec

For the NTP reference model

ฯƒ = 4 sec is what they chose to make room for the known quirks of the Unix operating-system kernel ๐Ÿงš. BUT bestie, faster workstations are coming, and they could be way more accurate, so to support those future upgrades it may be useful to drop ฯƒ to as little as one-tenth of the current value โœจ.

Heads up ๐Ÿ™ˆ: if you change ฯƒ, you also have to adjust K_f and K_g so the loop bandwidth stays the same. Specifically, the same ฯ‰_c and ฯ‰_z. Since ฮฑ varies as the reciprocal of ฯƒ, if ฯƒ is changed to something other than 22 (see Table 10), you divide both K_f and K_g by ฯƒ/4 to get the new values. Iconic math moment ๐Ÿ’….

Adjusting PLL Bandwidth (ฯ„)

Okay, the main thing about the type-II PLL design is that it can compensate for the intrinsic frequency errors of the local oscillator ๐Ÿฆ‹. It needs an initial period of adaptation to refine the frequency estimate (more on that in later sections of this appendix). The ฯ„ parameter sets the PLL time constant, and that sets the loop bandwidth, which is approximately equal to ฯ‰_c / ฯ„.

When you run with a relatively large bandwidth (small ฯ„), like in the analysis above, the PLL adapts quickly to changes in the input reference signal, but its long-term stability is poor ๐Ÿ˜ญ. So you can accumulate big errors if the system is cut off from the reference signal for a long time. When you run with a relatively small bandwidth (large ฯ„), the PLL adapts slowly to changes in the input reference signal, and it might not even lock onto it ๐Ÿ’€. Once the frequency estimate has stabilized, the PLL can coast for a long time without external corrections and without building up significant error โœจ.

To get the best performance without tailoring the loop bandwidth by hand (the main character move ๐Ÿ‘‘), each value of ฯ„ gets computed from the measured offset, delay and dispersion, as produced by the NTP protocol itself. The traditional way of doing this in precision timekeeping systems based on cesium clocks is to relate ฯ„ to the Allan variance. The Allan variance is defined as the mean of the first-order differences of sequential samples measured during a specified interval ฯ„:

ฯƒyยฒ(ฯ„) = 1 / (2(N โˆ’ 1)) ร— ฮฃ from i = 1 to Nโˆ’1 of [y(i + 1) โˆ’ y(i)]ยฒ

Here y is the fractional frequency measured with respect to the local timescale, and N is the number of samples ๐ŸŒธ.

In the NTP local-clock model, the Allan variance (called the compliance, h in Table 11) is approximated on a continuous basis. It's done by exponentially averaging the first-order differences of the offset samples, using an empirically determined averaging constant ๐ŸŽ€. The compliance then gets manipulated with somewhat ad hoc mapping functions (figured out from simulation and experience) to produce the loop time constant and the update interval. It's giving science experiment, but make it cute โœจ.

The NTP Clock Model

The PLL behavior can also be described by a set of recurrence equations, which depend on several variables and constants. The variables and parameters used in these equations are shown in Tables 9, 10 and 11. Lowkey, the powers of two are the real flex here ๐Ÿ“: they make implementation easier using arithmetic shifts, so you don't need multiply or divide capability.

Here's a capsule overview, so you know how it works ๐Ÿงš. The logical clock is continuously adjusted in small increments at fixed intervals of ฯƒ. The increments are determined while the variables in Tables 9 and 11 get updated. Those variables are computed from received NTP messages, as described in the NTP specification. Updates happen at discrete times, as each message is received. The intervals ฮผ between updates are variable and can range up to about 17 minutes โฐ. As part of update processing, the compliance h is computed and used to adjust the PLL time constant ฯ„. Finally, the update interval ฯ for transmitted NTP messages is a fixed multiple of ฯ„.

Updates are numbered from zero, and the neighborhood of the ith update is shown in Figure 13 ๐Ÿฆ‹. All variables start at i = 0 at zero, except the time constant ฯ„(0) = ฯ„, the poll interval ฮผ(0) = ฯ„ (from Table 10), and the compliance h(0) = K_s. After an interval ฮผ(i) (with i > 0) from the previous update, the ith update arrives at time t(i), including the time offset v_s(i). Then, after an interval ฮผ(i + 1), the (i + 1)th update arrives at time t(i + 1), including the time offset v_s(i + 1). When the update v_s(i) is received, the frequency error f(i + 1) and the phase error g(i + 1) are computed ๐Ÿ”ฅ:

f(i + 1) = f(i) + ฮผ(i) ยท v_s(i) / ฯ„(i)ยฒ
g(i + 1) = v_s(i) / ฯ„(i)

Note that these computations depend on the time constant ฯ„(i) and the poll interval ฮผ(i) computed earlier from the (i โˆ’ 1)th update ๐Ÿ‘€. Then the time constant for the next interval is computed from the current compliance h(i):

ฯ„(i + 1) = max[K_s โˆ’ |h(i)|, 1]

Next, using the new ฯ„ (called ฯ„โ€ฒ to avoid confusion ๐Ÿซถ), the poll interval is computed:

ฯ(i + 1) = K_u ยท ฯ„โ€ฒ

Finally, the compliance h(i + 1) is recomputed for use in the (i + 1)th update:

h(i + 1) = h(i) + (K_t ยท ฯ„โ€ฒ ยท v_s(i) โˆ’ h(i)) / K_h

The factor ฯ„โ€ฒ in the above adjusts the bandwidth of the PLL as a function of compliance, and it's honestly so clever ๐ŸŒˆ. When the compliance has been low over some relatively long period, ฯ„โ€ฒ increases and the bandwidth decreases. In this mode, small timing fluctuations due to network jitter get suppressed, and the PLL reaches the most accurate frequency estimate โœจ. On the other hand, if the compliance gets high because of greatly increased jitter or a systematic frequency offset, ฯ„โ€ฒ decreases and the bandwidth increases. In this mode the PLL is most adaptive to transients, like ones caused by a system reboot or a major timing error ๐Ÿ’ฅ. To keep things stable, the poll interval ฯ is varied directly with ฯ„.

A model suitable for simulation and parameter refinement can be built from the recurrence relations above ๐Ÿ’–. It's convenient to set the temporary variable a = g(i + 1). At each adjustment interval ฯƒ, the quantity a/K_g + f(i + 1)/K_f is added to the local-clock phase, and the quantity a/K_g is subtracted from a. For convenience, let n be the greatest integer in ฮผ(i)/ฯƒ. That's the number of adjustments that happen in the ith interval. So at the end of the ith interval, just before the (i + 1)th update, the VCO control voltage is:

v_c(i + 1) = v_c(i) + [1 โˆ’ (1 โˆ’ 1/K_g)^n] ยท g(i + 1) + (n / K_f) ยท f(i + 1)

Detailed simulation of the NTP PLL, using the values in Tables 9, 10 and 11 and the clock filter described in the NTP specification, gives these characteristics ๐Ÿ“Š. For a 100-ms phase change, the loop reaches zero error in 39 minutes, overshoots by 7 ms at 54 minutes, and settles to less than 1 ms in about six hours. For a 50-ppm frequency change, the loop reaches 1 ppm in about 16 hours and 0.1 ppm in about 26 hours. When the magnitude of correction exceeds a few milliseconds or a few ppm for more than a few updates, the compliance starts to increase. That makes the loop time constant and update interval decrease ๐Ÿ“‰. When the magnitude of correction falls below about 0.1 ppm for a few hours, the compliance starts to decrease, and the loop time constant and update interval increase ๐Ÿ“ˆ. The overall effect is a broad capture range exceeding 4 s per day, plus the ability to resolve oscillator skew well below 1 ms per day ๐ŸŒธ. These characteristics are appropriate for typical crystal-controlled oscillators, with or without temperature compensation or oven control ๐ŸŽ€.


Appendix H. Analysis of Errors and Correctness Principles โœจ

Introduction

This appendix is all about the errors that show up when NTP timestamps get generated and processed, and when delays and offsets get determined ๐Ÿ”. It sets up error bounds as a function of measured roundtrip delay and dispersion to the root (the primary reference source) of the synchronization subnet. It also talks about correctness assertions for those error bounds, plus the time-transfer, filtering and selection algorithms used in NTP ๐Ÿซถ.

The notation W = [u, v] describes the interval in which u is the lower limit and v is the upper limit, inclusive. So:

u = min(W) โ‰ค v = max(W)
W + a = [u + a, v + a]     (for scalar a)

Table 12 shows a summary of the other notation used in the analysis ๐Ÿ“. The notation <<x>> designates the (infinite) average of x, which is usually approximated by an exponential average. The notation xฬ‚ designates an estimator for x. The lowercase Greek letters ฮธ, ฮด and ฮต are used for measurement data for the local clock relative to a peer clock. The uppercase Greek letters ฮ˜, ฮ” and ฮ• are used for measurement data for the local clock relative to the primary reference source at the root of the synchronization subnet. Exceptions will be noted as they come up ๐Ÿ’….

Timestamp Errors

Okay, the standard second (1 s) is defined as 9,192,631,770 periods of the radiation corresponding to the transition between the two hyperfine levels of the ground state of the cesium-133 atom [ALL74b]. That implies a granularity of about 1.1x10-10 s. Other intervals can be determined as rational multiples of 1 s ๐Ÿง . NTP time has an inherent resolution of about 2.3x10-10 s, but local clocks usually have resolutions much worse than that. So the inherent error in resolving NTP time relative to the 1 s can be neglected ๐Ÿ™Œ.

In this analysis, the local clock is represented by a counter/divider that increments at intervals of s seconds. It's driven by an oscillator that runs at frequency f_c = n / s for some integer n ๐Ÿ”ง. A timestamp T(t) is determined by reading the clock at an arbitrary time t (the argument t will usually be left out for conciseness). Strictly speaking, s isn't known exactly, but it can be assumed bounded from above by the maximum reading error ฯ. The reading error itself is the random variable r, bounded by the interval [โˆ’ฯ, 0], where ฯ depends on the particular clock implementation ๐ŸŽ€. Readings of the same clock are almost always separated by intervals that are independent of, and much larger than, s. So successive readings can be treated as independent and identically distributed ๐Ÿ’œ.

The frequency error of the clock oscillator is the random variable f, bounded by the interval [โˆ’ฯ†, ฯ†], where ฯ† is the maximum frequency tolerance of the oscillator throughout its service life ๐ŸŒˆ. For f, the random variable is with respect to the population of all clocks. For any one clock, though, it usually changes only slowly over time, so it can usually be assumed constant for that clock ๐Ÿฆ‹. Thus, an NTP timestamp can be represented by the random variable T:

T = t + r + f ยท ฯ„

Here t is a clock reading, ฯ„ is the time interval since that reading, and minor approximations inherent in the measurement of ฯ„ are neglected ๐Ÿ’–.

To figure out the nature and expected size of timestamp errors, and the calculations based on them, it's useful to look at the probability density functions (pdf) p_r(x) and p_f(x) for r and f respectively. Assuming the clock

okay so the reading and counting processes are independent, which means the pdf for r is uniform over the interval <$E[-~rho ,~0]> ๐ŸŒธ. with conventional manufacturing processes and temperature variations, the pdf for f can be approximated by a truncated, zero-mean Gaussian distribution with standard deviation <$Esigma> ๐ŸŽ€. in conventional manufacturing processes, <$Esigma> is maneuvered so that the fraction of samples rejected outside the interval <$E[-~phi ,~phi ]> is acceptable ๐Ÿ’….

the pdf for the total timestamp error <$Eepsilon (x)> is the sum of the r and f contributions, computed as

<$Eepsilon (x)~ =~ int~from {- inf } to inf p sub r (t) p sub f (x~-~t)
d t>

which looks like a bell curve โœจ, symmetric about <$E-~rho over 2>, and bounded by the interval

<$E[ min (r)~+~min (f tau ),~max (r)~+~max (f tau )]~=~[-~rho ~-~phi tau
,~phi tau ]> .

since f changes only slowly over time for any single clock ๐Ÿฆ‹,

<$Eepsilon~==~[ min (r)~+~f tau ,~max (r)~+~f tau ]~=~ [-~ rho ,~0]~+~f
tau> ,

where <$Eepsilon> without an argument means the interval, and <$Eepsilon (x)> means the pdf ๐Ÿงš. in the following development, subscripts are used on various quantities to show which entity or timestamp the quantity applies to ๐Ÿ’–. occasionally <$Eepsilon> is used to mean an absolute maximum error instead of the interval, but you can tell from context ๐Ÿ™ˆ.

Measurement Errors

in NTP, the roundtrip delay and clock offset between two peers A and B are figured out by a procedure where timestamps get exchanged over the network paths between them ๐Ÿ”ฅ. the procedure uses the four most recent timestamps, numbered like in Figure 14<$&fig14>, where <$Etheta sub 0> is the true clock offset of peer B relative to peer A ๐Ÿ‘‘. the <$ET sub 1> and <$ET sub 4> timestamps are determined relative to the A clock, while <$ET sub 2> and <$ET sub 3> are determined relative to the B clock ๐Ÿซถ. the measured roundtrip delay <$Edelta> and clock offset <$Etheta> of B relative to A are

<$Edelta~=~(T sub 4~-~T sub 1 )~-~(T sub 3~-~T sub 2
)~~~~and~~~~theta~=~{(T sub 2~-~T sub 1 )~+~(T sub 3~-~T sub 4 )} over
2> .

the errors inherent in determining the timestamps T1, T2, T3 and T4 are, respectively,

<$Eepsilon sub 1~=~[-~rho sub A ,~0]>, <$E~epsilon sub 2~=~[-~rho sub B
,~0]>, <$E~epsilon sub 3~=~[-~rho sub B ,~0]~+~f sub B (T sub 3 ~-~T sub
2 )>, <$E~epsilon sub 4~=~[-~rho sub A ,~0]~+~f sub A (T sub 4 ~-~T sub
1 )> .

for specific peers A and B, where <$Ef sub A> and <$Ef sub B> can be treated as constants, the interval holding the maximum error in determining <$Edelta> is

<$E[ min ( epsilon sub 4 )~-~max ( epsilon sub 1 )~-~max ( epsilon sub 3
)~+~min ( epsilon sub 2 ),~ max ( epsilon sub 4 )~-~min ( epsilon sub 1
)~-~min ( epsilon sub 3 )~+~max ( epsilon sub 2 )]>
<$E=~[-~rho sub A~-~rho sub B ,~rho sub A ~+~rho sub B ]~+~f sub A (T
sub 4~-~T sub 1 )~-~f sub B (T sub 3~-~T sub 2 )> .

in the NTP local clock model, the residual frequency errors <$Ef sub A> and <$Ef sub B> get minimized using a type-II phase-lock loop (PLL) ๐ŸŒˆ. under most conditions these errors are small and can be ignored ๐Ÿ˜Œ. the pdf for the remaining errors is symmetric, so <$Edelta hat~=~<< delta >>> is an unbiased maximum-likelihood estimator for the true roundtrip delay, no matter the particular values of <$Erho sub A> and <$Erho sub B> โœจ.

however, to reliably bound the errors under all conditions of component variation and operational regimes, the PLL design and the tolerance of its intrinsic oscillator have to be controlled so that it is not possible under any circumstances for <$Ef sub A> or <$Ef sub B> to exceed the bounds <$E[-~phi sub A ,~phi sub A ]> or <$E[-~phi sub B ,~phi sub B ]>, respectively ๐Ÿ’€.

setting <$Erho~=~max ( rho sub A ,~rho sub B )> for convenience, the absolute maximum error <$Eepsilon sub delta> in determining roundtrip delay <$Edelta> is

<$Eepsilon sub delta~==~rho~+~phi sub A (T sub 4~-~T sub 1 )~+~phi sub B
(T sub 3~-~T sub 2 )> ,

neglecting residuals ๐Ÿ“.

just like for <$Edelta>, where <$Ef sub A> and <$Ef sub B> can be considered constants, the interval holding the maximum error in determining <$Etheta> is

<$E{[ min ( epsilon sub 2 )~-~max ( epsilon sub 1 )~+~min ( epsilon sub
3 )~-~max ( epsilon sub 4 ),~ max ( epsilon sub 2 )~-~min ( epsilon sub
1 )~+~max ( epsilon sub 3 )~-~min ( epsilon sub 4 )]} over 2>
<$E=~[ -~rho sub B ,~rho sub A ]~+~{f sub B (T sub 3~-~T sub 2 )~-~f sub
A (T sub 4 ~-~T sub 1 )} over 2> .

under most conditions the errors from <$Ef sub A> and <$Ef sub B> are small and can be ignored ๐Ÿฆ‹. if <$Erho sub A~=~rho sub B~=~rho>, meaning both the A and B clocks have the same resolution, the pdf for the remaining errors is symmetric, so <$Etheta hat~=~<< theta >>> is an unbiased maximum-likelihood estimator for the true clock offset <$Etheta sub 0>, no matter the particular value of <$Erho> ๐Ÿ’œ. if <$Erho sub A~!=~rho sub B>, then <$E<< theta >>> is not an unbiased estimator ๐Ÿ™ƒ. still, the bias error is on the order of

<$E{rho sub A~-~rho sub B } over 2> .

and can usually be neglected ๐Ÿ’….

again setting <$Erho~=~max ( rho sub A ,~rho sub B )> for convenience, the absolute maximum error <$Eepsilon sub theta> in determining clock offset <$Etheta> is

<$Eepsilon sub theta~==~{rho~+~phi sub A (T sub 4~-~T sub 1 )~+~phi sub
B (T sub 3~-~T sub 2 )} over 2 > .

Network Errors

in practice, errors from stochastic network delays usually dominate ๐Ÿ˜ญ. in general, network delays can't be characterized as a stationary random process, because network queues grow and shrink in chaotic ways and arriving customer traffic is often bursty ๐ŸŒช๏ธ. however, it's a simple exercise to calculate bounds on clock offset errors as a function of measured delay ๐Ÿง . let <$ET sub 2~-~T sub 1~=~a> and <$ET sub 3~-~T sub 4~=~b>. then,

<$Edelta~=~a~-~b~~~~ and ~~~~theta~=~{a~+~b} over 2> .

the true offset of B relative to A is called <$Etheta sub 0> in Figure 14. let x be the actual delay between a message leaving A and arriving at B. so <$Ex~+~theta sub 0~=~T sub 2~-~T sub 1~==~a>. since x has to be positive in our universe, <$Ex~=~a~-~theta sub 0~>>=~0>, which requires <$Etheta sub 0~<<=~a>. a similar argument requires that <$Eb~<<=~theta sub 0>, so surely <$Eb~<<=~theta sub 0~<<=~a> ๐ŸŒŸ. this inequality can also be written as

<$Eb~=~{a~+~b} over 2~-~{a~-~b} over 2~<<=~theta sub 0~<<=~{a~+~b} over
2~+~{a~-~b} over 2~=~a> ,

which is equivalent to

<$Etheta~-~delta over 2~<<=~theta sub 0~<<=~theta~+~delta over 2> .

in the previous section we figured out bounds on delay and offset errors ๐ŸŽ€. so the inequality can be written

<$Etheta~-~epsilon sub theta~-~{delta~+~epsilon sub delta} over
2~<<=~theta sub 0~<<=~theta~+~epsilon sub theta~+~{delta~+~ epsilon sub
delta } over 2> ,

where <$Eepsilon sub theta> is the maximum offset error and <$Eepsilon sub delta> is the maximum delay error derived earlier. the quantity

<$Eepsilon~=~epsilon sub theta~+~epsilon sub delta over 2~=~rho~+~phi
sub A (T sub 4~-~T sub 1 )~+~phi sub B (T sub 3~-~T sub 2 )> ,

called the peer dispersion, defines the maximum error in the inequality ๐Ÿ”ฅ. so the correctness interval I can be defined as the interval

<$EI~=~[ theta~-~delta over 2~-~epsilon ,~theta~+~delta over 2~+~epsilon
]> ,

in which the clock offset <$EC~=~theta> is the midpoint ๐Ÿ‘‘. by construction, the true offset <$Etheta sub 0> must lie somewhere in this interval ๐Ÿ’–.

Inherited Errors

as the NTP specification describes, the NTP time server keeps the local clock <$ETHETA>, along with the root roundtrip delay <$EDELTA> and root dispersion <$EEPSILON> relative to the primary reference source at the root of the synchronization subnet ๐ŸŒณ. these values are either included in each update message or can be derived as the NTP specification describes ๐Ÿ“ฆ. on top of that, the protocol exchange and clock-filter algorithm give the clock offset <$Etheta> and roundtrip delay <$Edelta> of the local clock relative to the peer clock, plus various error accumulations as described below ๐Ÿง. the following discussion works out how errors from the time-transfer process pile up within the subnet and add to the overall error budget at each server ๐Ÿซง.

an NTP measurement update has three parts: clock offset <$Etheta>, roundtrip delay <$Edelta>, and maximum error or dispersion <$Eepsilon> of the local clock relative to a peer clock ๐Ÿญ. for a primary clock update these values are usually all zero, although <$Eepsilon> can be tailored to reflect the specified maximum error of the primary reference source itself ๐ŸŒท. otherwise <$Etheta> and <$Edelta> are calculated directly from the four most recent timestamps, as the NTP specification describes ๐Ÿงฎ. the dispersion <$Eepsilon> includes these contributions:

  1. each time the local clock is read, a reading error happens because of the finite granularity or precision of the implementation ๐Ÿ˜ตโ€๐Ÿ’ซ. this is called the measurement dispersion <$Erho>.
  2. once an offset is determined, an error from frequency offset or skew builds up over time ๐Ÿ“ˆ. this is called the skew dispersion <$Ephi tau>, where <$Ephi> is the skew-rate constant (<$Eroman NTP.MAXSKEW over NTP.MAXAGE> in the NTP specification) and <$Etau> is the interval since the dispersion was last updated โณ.
  3. when a series of offsets are determined at regular intervals and collected in a window of samples, as in the NTP clock-filter algorithm, the (estimated) extra error from offset sample variance is called the filter dispersion <$Eepsilon sub sigma> ๐Ÿงบ.
  4. when a bunch of peers are considered for synchronization and two or more are determined to be correctly synchronized to a primary reference source, as in the NTP clock-selection algorithm, the (estimated) extra error from offset sample variance is called the selection dispersion <$Eepsilon sub xi> ๐ŸŽฏ.

Figure 15<$&fig15> shows how these errors pile up in the normal course of NTP processing ๐Ÿฆข. received messages from a single peer are represented by the packet variables. from the four most recent timestamps T1, T2, T3 and T4, the clock offset and roundtrip delay sample for the local clock relative to the peer clock are calculated directly ๐Ÿ’ซ. the message also includes the root roundtrip delay <$EDELTA prime> and root dispersion <$EEPSILON prime> of the peer itself; however, before sending, the peer adds the measurement dispersion <$Erho> and skew dispersion <$Ephi tau>, where those quantities are determined by the peer and <$Etau> is the interval according to the peer clock since its clock was last updated ๐ŸŒˆ.

the NTP clock-filter procedure saves the most recent samples <$Etheta sub i> and <$Edelta sub i> in the clock filter, as the NTP specification describes ๐Ÿ“š. the quantities <$Erho> and <$Ephi> characterize the local clock's maximum reading error and frequency error, respectively ๐ŸŽˆ. each sample includes the dispersion

<$Eepsilon sub i~=~rho~+~phi (T sub 4~-~T sub 1 )>,

which is set when it arrives ๐Ÿฌ. each time a new sample arrives, all samples in the filter are updated with the skew dispersion <$Ephi tau sub i>, where <$Etau sub i> is the interval since the last sample arrived, as recorded in the variable peer.update โฑ๏ธ. the clock-filter algorithm determines the selected clock offset <$Etheta> (peer.offset), along with the associated roundtrip delay <$Edelta> (peer.delay) and filter dispersion <$Eepsilon sub sigma>, which is added to the associated sample dispersion <$Eepsilon sub i> to form the peer dispersion <$Eepsilon> (peer.dispersion) ๐Ÿ’….

the NTP clock-selection procedure selects a single peer to become the synchronization source, as the NTP specification describes ๐Ÿซถ. the operation of the algorithm determines the final clock offset <$ETHETA>

So the final numbers are the clock offset ฮ˜ (local clock), roundtrip delay ฮ” (sys.rootdelay), and dispersion ฮต (sys.rootdispersion), all measured relative to the root of the synchronization subnet. That's the whole picture, and it's in Figure 15 โœจ. Two things to notice. First, the selected peer's dispersion gets added together with the skew that's built up since it was last updated, plus the select dispersion ฮต_ฮพ that the clock-select algorithm computes itself ๐Ÿงฎ. Second, the final clock offset ฮ˜ isn't measured from the root. It comes from the local clock's offset relative to the peer clock ๐Ÿซถ. That's on purpose, because it keeps the whole subnet stable ๐ŸŒธ.

There's one more shortcut. The packet variables ฮ”โ€ฒ and ฮ•โ€ฒ come from the latest message received, not from the exact moment the clock-filter picked its offset ๐Ÿ’…. Tiny errors from that get ignored, which is totally fine. So the total dispersion building up relative to the root is:

EPSILON = epsilon + phi tau + epsilon_xi + |THETA| + EPSILON prime

Here ฯ„ is the time since the peer variables were last updated, and |ฮ˜| is the starting absolute error when the local clock was set ๐Ÿ•ฐ๏ธ.

The three values (offset, roundtrip delay and dispersion) all just add up ๐ŸŽ€. If ฮ˜_i, ฮ”_i and ฮ•_i are the values at peer i relative to the root, then peer j's values at time t are:

THETA_j(t) = THETA_i + theta_j(t)
DELTA_j(t) = DELTA_i + delta_j
EPSILON_j(t) = EPSILON_i + epsilon_i + epsilon_j(t)

So peer j's clock offset, roundtrip delay and dispersion at time t are these. The time-dependent bits, ฮธ_j(t) and ฮต_j(t), capture the local-clock correction and dispersion built up since the last update arrived from peer i ๐Ÿฆ‹. The ฮต_i term is the dispersion peer i has picked up from the moment its clock was last set until the latest update it sent to peer j ๐Ÿ’œ. Here's the catch: you can figure out the local clock's offset relative to the peer clock directly, but the offset relative to the root can only be estimated probabilistically, and only within the bounds from this section and the one before it ๐Ÿ™ˆ.

The NTP synchronization subnet is shaped like a tree with the primary server(s) at the root ๐ŸŒณ. That means every time server has an unbroken path up to the primary reference source. Accuracy and stability both depend on the synchronization distance ฮ›, which is defined as:

LAMBDA = EPSILON + DELTA / 2

The selection algorithm loves the minimum-distance paths, so it picks those and gets the best accuracy and stability ๐Ÿ‘‘. Since ฮ˜_0, ฮ”_0 and ฮ•_0 are all zero, you can just add up the clock offsets, roundtrip delays and dispersions of each server along the minimum-distance path from the root to server i. That sum gives you the clock offset ฮ˜_i, roundtrip delay ฮ”_i and dispersion ฮ•_i that server i inherits and carries as its own ๐Ÿงš.

Correctness Principles

To keep errors from bad clocks low and the service reliable, NTP leans on multiple peers and disjoint peer paths whenever it can ๐Ÿซ‚. Earlier in the development it was shown that if the primary reference source at the root is actually a correct clock, then the true offset ฮ˜_0 relative to that clock has to sit somewhere inside this interval:

[THETA - LAMBDA, THETA + LAMBDA] = [THETA - EPSILON - DELTA/2, THETA + EPSILON + DELTA/2]

With lots of clocks, it's not obvious upfront which ones are right and which are wrong ๐Ÿ˜ญ. Thankfully, the clustering and filtering techniques we talked about before give a high chance of spotting and tossing out the bad ones ๐Ÿ’–. Marzullo and Owicki [MAR85] came up with an algorithm to find a good interval containing the correct time, given the confidence intervals of m clocks, where at most f of them might be incorrect. It finds the smallest single intersection that contains every point lying in at least m โˆ’ f of the confidence intervals ๐Ÿ”ฅ.

Figure 16<$&fig16> walks through this with four clocks: A, B, C and D. Each one has its calculated time (shown by the <F128>๏ฟฝ<F255> symbol) and its own confidence interval. These intervals come from the earlier sections of this appendix ๐Ÿ“š. Any point in A's interval could be the real time for that clock. If every clock were correct, there'd be a nonempty intersection that includes all four intervals. Clearly that's not what's happening in this scenario ๐Ÿ˜ฌ. But if you assume one clock is wrong (say D), you might find a nonempty intersection covering all but one of the intervals ๐Ÿ’…. If that doesn't work, you try all but two, and so on ๐ŸŒˆ.

The Digital Time Service [DEC89] uses an algorithm from DEC that follows these exact principles ๐Ÿ’œ. For the Figure 16 scenario, it computes the interval for m = 4 clocks, three of which are actually correct and one of which isn't โœจ. Here's how it finds the low endpoint of the intersection. You start f at the number of presumed incorrect clocks (zero in this case) and set a counter i to zero ๐Ÿง. Going from the lowest endpoint upward, you add 1 to i at every low endpoint and subtract 1 at every high endpoint, and you stop when i >= m โˆ’ f. The counter tracks how many intervals overlap, which is also the number of presumed correct clocks ๐Ÿฆ‹. In the example the counter never reaches four, so f goes up by one and you run the whole thing again ๐Ÿ”. This time the counter hits three and stops at the low endpoint of the intersection, marked DTS in the figure. The upper endpoint is found with a similar procedure ๐ŸŒท.

This algorithm always finds the smallest single intersection containing points from at least one of the original m โˆ’ f confidence intervals, as long as the number of incorrect clocks is less than half the total, f < m/2 ๐Ÿ˜Œ. But here's the wrinkle: some points in that intersection might not be in all m โˆ’ f of the original intervals, and some or all of the calculated times (like C's in Figure 16) can land outside the intersection ๐Ÿ™ˆ. So the NTP clock-selection procedure tweaks the algorithm to make sure the intersection includes at least m โˆ’ f calculated times ๐ŸŽ€. In this modified version, a counter c starts at zero. Starting from either endpoint, c goes up by one at each calculated time. But neither f nor c gets reset between finding the low and high endpoints โญ. Once both endpoints are found, if c < f, then f goes up by one and the whole procedure repeats ๐Ÿ”. The revised algorithm finds the smallest intersection of m โˆ’ f intervals that contains at least m โˆ’ f calculated times ๐Ÿ’–. As Figure 16 shows, the modified version gives the intersection marked NTP, which includes the calculated time for C ๐Ÿ‘‘.

In the NTP clock-selection procedure, the peers whose clocks land in the final intersection (called the survivors) get put on a candidate list ๐Ÿ“. In the remaining steps, one or more survivors can be dropped from the list as outliers ๐Ÿ™…. Finally, the clock-combining algorithm in Appendix F takes a weighted average of whatever survivors are left, weighted by synchronization distance ๐Ÿงฎ. The result is a synthetic peer whose offset sits between the maximum and minimum offsets of the remaining survivors ๐Ÿฆ‹. That gives the clock offset ฮ˜, total roundtrip delay ฮ” and total dispersion ฮ• that the local clock inherits ๐Ÿซถ. In principle, the operating system could pass these values along through its time interface to the user, so the user could check how good the readings are for themselves ๐ŸŒธ.


Appendix I. Selected C-Language Program Listings

Okay, here come the C listings for some of the selected algorithms in the NTP specification ๐Ÿ’ป. They've been tested in a software simulator using data collected from regular operation, so they work ๐Ÿ”ฅ. But they're not necessarily a standard implementation, since lots of other implementations could also conform to the NTP specification ๐ŸŒˆ. So think of them as one valid take, not the one true way ๐Ÿ’….

Common Definitions and Variables

These definitions are shared by all procedures and peers ๐ŸŽ€.

#define NMAX 40                                 /* max clocks */
#define FMAX 8                                  /* max filter size */
#define HZ 1000                                 /* clock rate */
#define MAXSTRAT 15                             /* max stratum */
#define MAXSKEW 1                               /* max skew error per
MAXAGE */
#define MAXAGE 86400                            /* max clock age */
#define MAXDISP 16                              /* max dispersion */
#define MINCLOCK 3                              /* min survivor clocks
*/
#define MAXCLOCK 10                             /* min candidate clocks
*/
#define FILTER .5                                       /* filter weight
*/
#define SELECT .75                              /* select weight */

These are the peer state variables, one set for each peer ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘:

double filtp[NMAX][FMAX];                               /* offset
samples */
double fildp[NMAX][FMAX];                       /* delay samples */
double filep[NMAX][FMAX];                       /* dispersion samples */
double tp[NMAX];                                        /* offset */
double dp[NMAX];                                        /* delay */
double ep[NMAX];                                        /* dispersion */
double rp[NMAX];                                        /* last offset
*/
double utc[NMAX];                                       /* update tstamp
*/
int st[NMAX];                                           /* stratum */

And these are the system state variables and constants ๐ŸŒŸ:

double rho = 1./HZ;                                     /* max reading
error */
double phi = MAXSKEW/MAXAGE;            /* max skew rate */
double bot, top;                                        /* confidence
interval limits */
double theta;                                           /* clock offset
*/
double delta;                                           /* roundtrip
delay */
double epsil;                                           /* dispersion */
double tstamp;                                  /* current time */
int source;                                             /* clock source
*/
int n1, n2;                                             /* min/max clock
ids */

Last up, the temporary lists shared by all peers and procedures ๐Ÿงƒ:

double list[3*NMAX];                            /* temporary list*/
int index[3*NMAX];                                      /* index list */

Clock Filter Algorithm

Here's the clock filter, the thing that cleans up the samples ๐Ÿงผ:

/*
   clock filter algorithm

   n = peer id, offset = sample offset, delay = sample delay, disp =
sample dispersion;
   computes tp[n] = peer offset, dp[n] = peer delay, ep[n] = peer
dispersion
 */

void filter(int n, double offset, double delay, double disp) {

        int i, j, k, m;                                 /* int temps */
        double x;                                       /* double temps
*/

        for (i = FMAX-1; i > 0; i--) {                  /*
update/shift filter */
                filtp[n][i] = filtp[n][i-1]; fildp[n][i] =
fildp[n][i-1];
                filep[n][i] = filep[n][i-1]+phi*(tstamp-utc[n]);
                }
        utc[n] = tstamp; filtp[n][0] = offset-tp[0]; fildp[n][0] =
delay; filep[n][0] = disp;
        m = 0;                                          /*
construct/sort temp list */
        for (i = 0; i << FMAX; i++) {
                if (filep[n][i] >>= MAXDISP) continue;
                list[m] = filep[n][i]+fildp[n][i]/2.; index[m] = i;
                for (j = 0; j << m; j++) {
                        if (list[j] >> list[m]) {
                                x = list[j]; k = index[j]; list[j] =
list[m]; index[j] = index[m];
                                list[m] = x; index[m] = k;
                                }
                        }
                m = m+1;
                }

        if (m <<= 0) ep[n] = MAXDISP;           /* compute filter
dispersion */
        else {
                ep[n] = 0;
                for (i = FMAX<196>1; i >>= 0; i<196> <196>) {
                        if (i << m) x =
fabs(filtp[n][index[0]]<196>filtp[n][index[i]]);
                        else x = MAXDISP;
                        ep[n] = FILTER*(ep[n]+x);
                        }
                i = index[0]; ep[n] = ep[n]+filep[n][i]; tp[n] =
filtp[n][i]; dp[n] = fildp[n][i];
                }
        return;
        }

Interval Intersection Algorithm โœจ

okay bestie, now we're getting into the intersection stuff ๐Ÿ’– this bit lines up every clock's window and hunts for the spot where the most of them overlap ๐Ÿฆ‹

/*
   compute interval intersection

   computes bot = lowpoint, top = highpoint (bot >> top if no
intersection)
*/

void dts() {

        int f;                                          /* intersection
ceiling */
        int end;                                        /* endpoint
counter */
        int clk;                                                /*
falseticker counter */
        int i, j, k, m, n;                              /* int temps */
        double x, y;                                    /* double temps
*/

        m = 0; i = 0;
        for (n = n1; n <<= n2; n++) {   /* construct endpoint list */
                if (ep[n] >>= MAXDISP) continue;
                m = m+1;
                list[i] = tp[n]<196>dist(n); index[i] = <196>1; /*
lowpoint */
                for (j = 0; j << i; j++) {
                        if ((list[j] >> list[i]) || ((list[j] ==
list[i]) && (index[j] >> index[i]))) {
                                x = list[j]; k = index[j]; list[j] =
list[i]; index[j] = index[i];
                                list[i] = x; index[i] = k;
                                }
                        }
                i = i+1;

                list[i] = tp[n]; index[i] = 0;          /* midpoint */
                for (j = 0; j << i; j++) {
                        if ((list[j] >> list[i]) || ((list[j] ==
list[i]) && (index[j] >> index[i]))) {
                                x = list[j]; k = index[j]; list[j] =
list[i]; index[j] = index[i];
                                list[i] = x; index[i] = k;
                                }
                        }
                i = i+1;

                list[i] = tp[n]+dist(n); index[i] = 1;  /* highpoint */
                for (j = 0; j << i; j++) {
                        if ((list[j] >> list[i]) || ((list[j] ==
list[i]) && (index[j] >> index[i]))) {
                                x = list[j]; k = index[j]; list[j] =
list[i]; index[j] = index[i];
                                list[i] = x; index[i] = k;
                                }
                        }
                i = i+1;
                }

        if (m <<= 0) return;
        for (f = 0; f << m/2; f++) {                    /* find
intersection */
                clk = 0; end = 0;                       /* lowpoint */
                for (j = 0; j << i; j++) {
                        end = end<196>index[j]; bot = list[j];
                        if (end >>= (m<196>f)) break;
                        if (index[j] == 0) clk = clk+1;
                        }
                end = 0;                                /* highpoint */
                for (j = i<196>1; j >>= 0; j<196> <196>) {
                        end = end+index[j]; top = list[j];
                        if (end >>= (m<196>f)) break;
                        if (index[j] == 0) clk = clk+1;
                        }
                if (clk <<= f) break;
                }
        return;
        }

Clockโ€“Selection Algorithm ๐ŸŽ€

next up is the selection step, where we pick the best crew of clocks from the candidate list ๐Ÿ‘‘ it's giving main character energy, no cap ๐Ÿ’…

/*
   select best subset of clocks in candidate list

   bot = lowpoint, top = highpoint; constructs index = candidate index
list,
   m = number of candidates, source = clock source,
   theta = clock offset, delta = roundtrip delay, epsil = dispersion
*/

void select() {

        double xi;                                      /* max select
dispersion */
        double eps;                                     /* min peer
dispersion */
        int i, j, k, n;                                 /* int temps */
        double x, y, z;                         /* double temps */

        m = 0;
        for (n = n1; n <<= n2; n++) {   /* make/sort candidate list */
                if ((st[n] >> 0) && (st[n] << MAXSTRAT) && (tp[n] >>=
bot) && (tp[n] <<= top)) {
                        list[m] = MAXDISP*st[n]+dist(n); index[m] = n;
                        for (j = 0; j << m; j++) {
                                if (list[j] >> list[m]) {
                                        x = list[j]; k = index[j];
list[j] = list[m]; index[j] = index[m];
                                        list[m] = x; index[m] = k;
                                        }
                                }
                        m = m+1;
                        }
                }
        if (m <<= 0) {
                source = 0; return;
                }
        if (m >> MAXCLOCK) m = MAXCLOCK;

        while (1) {                                     /* cast out
falsetickers */
                xi = 0.; eps = MAXDISP;
                for (j = 0; j << m; j++) {
                        x = 0.;
                        for (k = m<196>1; k >>= 0; k<196> <196>)
                                x =
SELECT*(x+fabs(tp[index[j]]<196>tp[index[k]]));
                        if (x >> xi) {
                                xi = x; i = j;          /* max(xi) */
                                }
                        x = ep[index[j]]+phi*(tstamp<196>utc[index[j]]);
                        if (x << eps) eps = x;          /* min(eps) */
                        }
                if ((xi <<= eps) || (m <<= MINCLOCK)) break;
                if (index[i] == source) source = 0;
                for (j = i; j << m<196>1; j++) index[j] = index[j+1];
                m = m<196>1;
                }

        i = index[0];                                   /* declare
winner */
        if (source != i)
                if (source == 0) source = i;
                else if (st[i] << st[source]) source = i;
        theta = combine(); delta = dp[i]; epsil =
ep[i]+phi*(tstamp<196>utc[i])+xi;
        return;
        }

Clockโ€“Combining Procedure ๐Ÿ’œ

once the squad is picked, we blend their offsets into one weighted average ๐ŸŒˆ teamwork makes the dream work, fr fr ๐Ÿซถ

/*
   compute weighted ensemble average

   index = candidate index list, m = number of candidates; returns
combined clock offset
*/

double combine() {

        int i;                                          /* int temps */
        double x, y, z;                         /* double temps */
        z = 0. ; y = 0.;
        for (i = 0; i << m; i++) {                      /* compute
weighted offset */
                j = index[i]; x = dist(j)); z = z+tp[j]/x; y = y+1./x;
                }
        return z/y;                                     /* normalize */
        }

Subroutine to Compute Synchronization Distance ๐Ÿฆ‹

last but not least, this tiny helper figures out the synchronization distance for each peer ๐Ÿงš short and sweet, just like my texts to you ๐Ÿ’–

/*
   compute synchronization distance

   n = peer id; returns synchronization distance
 */

double dist(int n) {

        return ep[n]+phi*(tstamp<196>utc[n])+fabs(dp[n])/2.;
        }

Security considerations ๐Ÿ’€

okay this is the serious part, so no skipping bestie ๐Ÿ™ˆ the security stuff lives in Section 3.6 and Appendix C, so go read those ๐Ÿซถ

Author's address ๐ŸŒธ

David L. Mills
Electrical Engineering Department
University of Delaware
Newark, DE 19716
Phone (302) 451<196>8247
EMail mills@udel.edu
๐Ÿ“ฆ where this page comes from