on master help sign in
Stop passing TTL in gossip events; receivers use server-configured TTL
commit06c2ada5025986c8b459bb018208a8af819d9a1b authoriko <mail@iko.soy> dateMon 4 May 2026, 12:36 (5 months ago) parent32dd952
1 file changed+21−27
server.knot+21−27
server.knot+21−27
@@ -50,9 +50,6 @@ federationScheme = "https://"
5050-- long after `receivedAt`; expired rows are pruned by `pruneExpired` (30 days).
5151messageMaxAge : Int<Ms>
5252messageMaxAge = 2592000000
53−-- Upper bound on TTL a client may request for a presence entry (1 hour).
54−maxPresenceTtl : Int<Ms>
55−maxPresenceTtl = 3600000
5653-- Length (chars) of a hex-encoded Ed25519 pubkey (32 bytes → 64 hex).
5754pubkeyLen : Int
5855pubkeyLen = 64
@@ -168,12 +165,11 @@ authGlobalRateWindowMs = 1000
168165-- How long a session token remains valid after issue (1 hour).
169166sessionExpiry : Int<Ms>
170167sessionExpiry = 3600000
171−-- How long an "online" presence gossip is considered fresh (90 min).
168+-- How long a received "online" presence gossip is considered fresh (90 min).
169+-- Receivers anchor the TTL to their own wall clock at receive time, so this
170+-- value isn't carried on the wire.
172171onlineGossipTtl : Int<Ms>
173172onlineGossipTtl = 5400000
174−-- Floor on TTL accepted for "offline" presence gossip (1 min).
175−offlineGossipMinTtl : Int<Ms>
176−offlineGossipMinTtl = 60000
177173-- Times to retry a federation forward before giving up.
178174maxForwardRetries : Int
179175maxForwardRetries = 10
@@ -262,7 +258,6 @@ type GossipEvent = {
262258 pubkey: PubkeyHex,
263259 server: ServerName,
264260 timestamp: Timestamp,
265− ttl: Int<Ms>,
266261 seenBy: [Text]
267262}
268263
@@ -995,10 +990,6 @@ clearFailure = \server -> atomic (do
995990-- sequence generation, presence lookup, duplicate detection across
996991-- (messages, deliveredIds), and last-write-wins presence merge.
997992
998−-- Clamp a client-supplied TTL to (0, maxPresenceTtl].
999−clampTtl : Int<Ms> -> Int<Ms>
1000−clampTtl = clamp 1 maxPresenceTtl
1001−
1002993-- Strictly-monotonic next sequence timestamp: max of wall clock and prior+1.
1003994-- Guarantees /poll cursors order events even when many arrive in the same ms.
1004995nextSeqTs : Timestamp -> Timestamp -> Timestamp
@@ -1081,13 +1072,12 @@ pruneExpired = \t -> atomic do
10811072-- both subsystems back off together.
10821073
10831074-- Build a gossip event with this server's identity.
1084−mkGossip : GossipEventType -> PubkeyHex -> Timestamp -> Int<Ms> -> GossipEvent
1085−mkGossip = \etype pk t ttl -> {
1075+mkGossip : GossipEventType -> PubkeyHex -> Timestamp -> GossipEvent
1076+mkGossip = \etype pk t -> {
10861077 eventType: etype,
10871078 pubkey: pk,
10881079 server: serverHostN,
10891080 timestamp: t,
1090− ttl,
10911081 seenBy: [serverHostN]
10921082}
10931083
@@ -1116,7 +1106,7 @@ sendBatchedGossip = \peers events -> forEach peers (\server -> do
11161106broadcastOfflineGossip : Int<Ms> -> [{pubkey: PubkeyHex | a}] -> IO {r *presence} {}
11171107broadcastOfflineGossip = \t expiredLocal -> do
11181108 presence <- *presence
1119− when (federationEnabled && count expiredLocal > 0) (sendBatchedGossip (knownPeerServersFromPresence presence) (pluck expiredLocal (\p -> mkGossip (GossipOffline {}) p.pubkey t offlineGossipMinTtl)))
1109+ when (federationEnabled && count expiredLocal > 0) (sendBatchedGossip (knownPeerServersFromPresence presence) (pluck expiredLocal (\p -> mkGossip (GossipOffline {}) p.pubkey t)))
11201110
11211111-- ============================================
11221112-- Forward retries
@@ -1219,7 +1209,7 @@ refreshLocalPresence = \t -> do
12191209 let updated = union (removeWhere presence needsRefresh) refreshed
12201210 replace *presence = updated
12211211 yield {refreshed, peers: knownPeerServersFromPresence updated})
1222− when (count refreshState.refreshed > 0) (fork (sendBatchedGossip refreshState.peers (pluck refreshState.refreshed (\p -> mkGossip (GossipOnline {}) p.pubkey t (clampTtl (sessionExpiry / 1000))))))
1212+ when (count refreshState.refreshed > 0) (fork (sendBatchedGossip refreshState.peers (pluck refreshState.refreshed (\p -> mkGossip (GossipOnline {}) p.pubkey t))))
12231213
12241214-- ============================================
12251215-- Handler: challenge / verify / revoke / logout
@@ -1317,7 +1307,7 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp respond ->
13171307 then yield (respond {token: "", expiresAt: 0, error: "invalid"})
13181308 else do
13191309 when federationEnabled (fork (sendBatchedGossip result.peers [
1320− mkGossip (GossipOnline {}) pubkey t onlineGossipTtl
1310+ mkGossip (GossipOnline {}) pubkey t
13211311 ]))
13221312 yield (respond {token, expiresAt: expiry, error: ""})
13231313
@@ -1358,7 +1348,7 @@ handleLogout = \authorization clientIp respond -> do
13581348 yield (knownPeerServersFromPresence pres)
13591349 yield {ok: True {}, pubkey, peers})
13601350 when (removed.ok && federationEnabled) (fork (sendBatchedGossip removed.peers [
1361− mkGossip (GossipOffline {}) removed.pubkey t offlineGossipMinTtl
1351+ mkGossip (GossipOffline {}) removed.pubkey t
13621352 ]))
13631353 yield (respond {removed.ok, authorized: removed.ok})
13641354
@@ -1769,31 +1759,35 @@ handleFederationPubkey = \respond -> do
17691759-- skew tolerance, applies last-write-wins merge per (pubkey, server), and
17701760-- — for keys that became newly online — flushes any locally-buffered messages
17711761-- for them via `forwardQueuedToServer` (the catch-up path).
1772−handleRecvGossip : [{eventType: <GossipOffline {} | GossipOnline {} | a>, pubkey: PubkeyHex, server: Text, timestamp: Int<Ms>, ttl: Int<Ms> | b}] -> Int -> Text -> Int -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> c) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *presence, clock, network} c
1762+handleRecvGossip : [{eventType: <GossipOffline {} | GossipOnline {} | a>, pubkey: PubkeyHex, server: Text, timestamp: Int<Ms> | b}] -> Int -> Text -> Int -> Text -> ({error: Text, ok: Bool, retryAfterMs: Int} -> c) -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *presence, clock, network} c
17731763handleRecvGossip = \events hopsRemaining fromServer requestTimestamp authorization respond -> do
17741764 t <- now
17751765 withFederationAuth authorization t respond fromServer "gossip" requestTimestamp (clamp 0 maxGossipHops hopsRemaining >= 1 && isRemote fromServer && count events <= maxGossipEvents) (do
17761766 newlyOnline <- atomic (do
17771767 pres <- *presence
1768+ -- Clamp e.timestamp to receive time so a peer can't push it into the
1769+ -- future to game the offline-vs-online LWW comparison below. The skew
1770+ -- check is a redundant sanity filter against broken senders.
17781771 let validEvents = do
17791772 e <- events
17801773 where isRemote e.server && e.timestamp <= t + clockSkewTolerance
1781− let expiresAt = min e.timestamp t + clampTtl e.ttl * 1000
1782− where expiresAt > t
1783− yield {e.pubkey, e.server, expiresAt, e.timestamp, e.eventType}
1774+ let effTs = min e.timestamp t
1775+ yield {e.pubkey, e.server, effTs, e.eventType}
17841776 let isOnline = \v -> case v.eventType of
17851777 GossipOnline {} -> True {}
17861778 _ -> False {}
17871779 let isOffline = \v -> case v.eventType of
17881780 GossipOffline {} -> True {}
17891781 _ -> False {}
1782+ -- Receiver anchors expiresAt to its own clock + onlineGossipTtl; senders
1783+ -- don't carry a TTL on the wire.
17901784 let online = mapWhere validEvents isOnline (\v -> {
17911785 v.pubkey,
17921786 v.server,
1793− v.expiresAt,
1794− v.timestamp
1787+ expiresAt: t + onlineGossipTtl,
1788+ timestamp: v.effTs
17951789 })
1796− let offline = mapWhere validEvents isOffline (\v -> {v.pubkey, v.server, v.timestamp})
1790+ let offline = mapWhere validEvents isOffline (\v -> {v.pubkey, v.server, timestamp: v.effTs})
17971791 *presence = mergePresence (filter pres (\p -> none offline (\o -> o.pubkey == p.pubkey && o.server == p.server && o.timestamp > p.timestamp))) online
17981792 yield (mapWhere online (\e -> none pres (\p -> p.pubkey == e.pubkey && p.server == e.server && p.expiresAt >= e.expiresAt)) (\e -> {
17991793 e.pubkey,
@@ -1899,7 +1893,7 @@ handleDrain = \phase authorization respond -> withMaintAuth authorization (yield
18991893 *presence = removeWhere pres isLocalLive
19001894 *pollWakeup = t
19011895 yield {peers: knownPeerServersFromPresence pres, localKeys: filter pres isLocalLive})
1902− when (effectivePhase == 2 && count drainResult.localKeys > 0 && federationEnabled) (fork (sendBatchedGossip drainResult.peers (pluck drainResult.localKeys (\p -> mkGossip (GossipOffline {}) p.pubkey t offlineGossipMinTtl))))
1896+ when (effectivePhase == 2 && count drainResult.localKeys > 0 && federationEnabled) (fork (sendBatchedGossip drainResult.peers (pluck drainResult.localKeys (\p -> mkGossip (GossipOffline {}) p.pubkey t))))
19031897 yield (respond {ok: True {}, drained: count drainResult.localKeys, phase: effectivePhase}))
19041898
19051899-- POST /maintenance/undrain: clear drain state and resume normal operation.