Handle a wrong clock (ios#45, Android half) #65

Merged
paulhitt merged 1 commits from feat/handle-a-wrong-clock into main 2026-09-04 20:49:13 +00:00
paulhitt commented 2026-09-04 17:38:56 +00:00 (Migrated from gitlab.com)

A wrong device clock breaks a promise made to somebody else. A story
disappearing after an hour is a promise the SENDER made, and it is enforced on
the READER's device — so a reader whose clock runs behind keeps a message the
sender was told would be gone, and nobody involved can tell.

That asymmetry decides the design, and it is the whole of this change. Expiry
takes the LATER of the device's clock and the relay's, never either alone. A
device set back cannot extend a message's life, and a hostile relay claiming an
earlier time cannot either, because the other source overrules it. The
remaining failure — expiring early on a clock set forward — costs the reader
something they were about to lose anyway.

The skew is learned free, from the Date header every HTTP response already
carries. Observed in RelayClient.checked, which is the one funnel every relay
call passes through, and BEFORE the success check: a phone whose clock is wrong
enough to be refused is exactly the phone that needs to be told why.

Story and disappearing-message expiry on the app side now reads
DeviceClock.expirySeconds() rather than System.currentTimeMillis(), at all
seven sites.

Android needs synchronisation iOS does not. The Swift version is
nonisolated(unsafe) static var because its writers sit on one actor; here the
observation is written from whichever OkHttp thread finished a call and read
from the UI thread. The two fields are @Volatile and updated together under a
lock — a torn read of "skew from now, timestamp from an hour ago" would age out
an observation just made, or trust one that had not been.

SimpleDateFormat is built per call rather than shared, because it is not
thread-safe, with a fixed Locale.US and GMT: a formatter taking its locale
from the device parses nothing on a phone set to most of the world, and the
failure is silent — the header is simply never understood.

15 unit tests. Proven: replacing the rule with "trust the server" turns the
hostile-relay test and the never-earlier test red. App builds, every JVM suite
green.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

A wrong device clock breaks a promise made to somebody else. A story disappearing after an hour is a promise the SENDER made, and it is enforced on the READER's device — so a reader whose clock runs behind keeps a message the sender was told would be gone, and nobody involved can tell. That asymmetry decides the design, and it is the whole of this change. Expiry takes the LATER of the device's clock and the relay's, never either alone. A device set back cannot extend a message's life, and a hostile relay claiming an earlier time cannot either, because the other source overrules it. The remaining failure — expiring early on a clock set forward — costs the reader something they were about to lose anyway. The skew is learned free, from the `Date` header every HTTP response already carries. Observed in `RelayClient.checked`, which is the one funnel every relay call passes through, and BEFORE the success check: a phone whose clock is wrong enough to be refused is exactly the phone that needs to be told why. Story and disappearing-message expiry on the app side now reads `DeviceClock.expirySeconds()` rather than `System.currentTimeMillis()`, at all seven sites. Android needs synchronisation iOS does not. The Swift version is `nonisolated(unsafe) static var` because its writers sit on one actor; here the observation is written from whichever OkHttp thread finished a call and read from the UI thread. The two fields are `@Volatile` and updated together under a lock — a torn read of "skew from now, timestamp from an hour ago" would age out an observation just made, or trust one that had not been. `SimpleDateFormat` is built per call rather than shared, because it is not thread-safe, with a fixed `Locale.US` and GMT: a formatter taking its locale from the device parses nothing on a phone set to most of the world, and the failure is silent — the header is simply never understood. 15 unit tests. Proven: replacing the rule with "trust the server" turns the hostile-relay test and the never-earlier test red. App builds, every JVM suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
paulhitt commented 2026-09-04 17:38:56 +00:00 (Migrated from gitlab.com)

assigned to @paulhitt

assigned to @paulhitt
paulhitt commented 2026-09-04 20:49:11 +00:00 (Migrated from gitlab.com)

deleted the feat/app-lock-backoff branch. This merge request now targets the main branch

deleted the `feat/app-lock-backoff` branch. This merge request now targets the `main` branch
paulhitt commented 2026-09-04 20:49:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 5afa147712

mentioned in commit 5afa147712fadfe015a1002dbcbd6b8459c47ff8
paulhitt (Migrated from gitlab.com) merged commit 5afa147712 into main 2026-09-04 20:49:13 +00:00
paulhitt commented 2026-09-09 19:01:36 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Sign in to join this conversation.