Core Question
Core Question
An NTP exchange hands back four timestamps. What can you actually solve for from just those four numbers, and what has to be assumed to get there?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name all four timestamps and what event each one marks
- derive
offset()anddelay()from the two equations in the module docstring, not just quote the formulas - say which assumption makes two equations with three unknowns solvable
- explain why
validate_exchange()checkst4 < t1andt3 < t2and nothing else
Read Order
Read Order
- Read the module docstring's derivation top to bottom
- Read
Exchange - Read
validate_exchange() - Read
offset() - Read
delay() - Run
examples/ntp/session_01_walkthrough.py
Read It Like Code
Read It Like Code
Exchange(
t1, # client transmit time
t2, # server receive time
t3, # server transmit time
t4, # client receive time
)Fields That Matter
Fields That Matter
| Field | Why it matters |
|---|---|
t1 | When the client sent the request, by the client's own clock. |
t2 | When the server received it, by the server's clock -- the first timestamp that can carry clock offset. |
t3 | When the server sent the reply, by the server's clock. |
t4 | When the client received the reply, by the client's own clock again. |
Decision Flow
Decision Flow
t4 < t1 or t3 < t2 -> Malformed (not one coherent round trip) otherwise -> Valid (offset() and delay() are trustworthy)
Reading Lens
Reading Lens
The important move in this session is to stop treating offset() and delay() as formulas to memorize and start asking:
- which of the four timestamps came from the client's clock, and which from the server's?
- in the docstring's two equations (
t2 = t1 + d_out + theta,t4 = t3 + d_in - theta), which term is the thing NTP actually wants (theta), and which two terms does it have to get rid of to isolate it? - what happens to those equations if you add them versus subtract them?
Toy Model Boundary
Toy Model Boundary
Real NTP timestamps are 64-bit fixed-point values in the NTP era format (seconds since 1900, with a fractional part good to about 232 picoseconds). This toy uses plain integer milliseconds -- Exchange's own docstring says so directly -- so the arithmetic reads like arithmetic instead of like bit-shifting. There is no era rollover, no fractional precision, and no NTP header (no leap indicator, no version, no mode) here at all; this module is only the four-timestamp math from RFC 5905 section 8, nothing upstream or downstream of it.
Code Landmarks
Code Landmarks
The module docstring's derivation
This is the actual lesson, not decoration. t2 = t1 + d_out + theta says the server's receive timestamp is the client's send time, plus how long the packet took to arrive, plus however far the server's clock reads ahead of the client's. t4 = t3 + d_in - theta is the mirror image for the return trip -- note the sign flip on theta, because now it's the client's clock reading behind by the same amount it read ahead of on the way out. Two equations, three unknowns (d_out, d_in, theta) -- until symmetry (d_out = d_in = d) turns it into two equations, two unknowns, solvable by addition.
validate_exchange()
Two comparisons, nothing else. t4 < t1 means the client's own clock reports finishing before it started -- physically the client's clock, not the network, but still enough to poison the math. t3 < t2 is the same idea on the server's side. Neither check inspects t1 vs t2 or t3 vs t4, because a clock offset between client and server can legitimately put t2 before t1 or t4 before t3 -- that is exactly the thing offset() is trying to measure, so it cannot also be treated as an error.
offset()
((t2 - t1) + (t3 - t4)) / 2. Read the docstring derivation to see the addition step ((t2-t1) + (t4-t3) = 2*theta under symmetry) reshuffled algebraically into this exact expression.
delay()
(t4 - t1) - (t3 - t2). Total time elapsed by the client's clock, minus time the server spent thinking. Note that delay() never needs the symmetry assumption at all -- it falls out of the same two equations regardless of whether d_out equals d_in.
Failure Questions
Failure Questions
Use the source file to answer these:
- In the docstring's derivation, which term cancels out when you add
t2 - t1andt4 - t3together, and what assumption is required for it to cancel exactly (not approximately)? validate_exchange()does not comparet2againstt1. Give a valid (non-malformed) exchange wheret2 < t1, and explain why that is not a bug.- If the server's clock were exactly synchronized with the client's (
theta = 0), what wouldoffset()return for any well-formed exchange, and why does that follow from the formula without needing a special case in the code? delay()does not appear anywhere in the formula foroffset(). Why does the module still bother computing it?- Session 03 (
asymmetry.py) says the symmetry assumption "cannot be detected" from the exchange itself. Looking only atoffset.py, what information would you need that these four timestamps do not provide in order to check whetherd_out == d_in?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/ntp/session_01_walkthrough.py
The walkthrough hand-computes Exchange(1000, 1005, 1006, 1012) -- offset ((1005-1000)+(1006-1012))/2 = -0.5, delay (1012-1000)-(1006-1005) = 11 -- then checks a symmetric zero-offset exchange, two independently malformed exchanges (t4 < t1, then t3 < t2), and one more exchange whose offset the reader should hand-verify before trusting the printed value.
Done When
Done When
The learner can say all of the following without looking at notes:
- "Four timestamps, two unknowns worth solving for -- offset and delay -- and symmetry is what makes the system solvable."
- "
validate_exchange()only rejects timestamps that violate one clock's own internal ordering, never a comparison across clocks." - "
offset()anddelay()are pure arithmetic on the same two equations, read two different ways."
References
References
- RFC 5905 Section 8 (On-Wire Protocol / clock offset and round-trip delay calculations)
Continue