Skip to main content

car_sync/
relay.rs

1//! Relay transport — the account-scoped op-stream devices push/pull against
2//! (slice B3 of `docs/proposals/multi-device-sync.md`, §"Transport" +
3//! §"Stragglers and the single coherence knob" + §"Sync protocol surface").
4//!
5//! The [`Relay`] trait is the proposal's surface — `push(ops)`,
6//! `pull(since_frontier) → {ops, latest_checkpoint_ptr}`, `ack(frontier)`,
7//! `checkpoint_put/get`, `roster()` — as a **library trait** with two
8//! reference implementations: [`InMemoryRelay`] (tests, in-process
9//! coordination) and [`FsRelay`] (a shared directory — the realistic
10//! single-user two-Mac loopback). The network daemon surface (`sync.*`
11//! JSON-RPC + the Parslee-hosted backend) is B6; anything that speaks this
12//! trait is that surface's contract.
13//!
14//! ## What the relay validates (and what it can't)
15//!
16//! The relay holds one **chain per device** and admits a pushed op only if
17//! it *continues* that chain exactly: contiguous `seq`, `prev` linking the
18//! relay-held head, HLC advancing (a re-push of an already-held op dedups on
19//! `op_id`; a *different* op claiming a held `seq` is a
20//! [`RelayError::Fork`] — the B1 permanent-fork hazard surfaced as a
21//! runtime error at the transport). A device pushes only its **own** chain
22//! ([`RelayError::ForeignOps`]). In B3 payloads are cleartext so the relay
23//! can run this full verification; under B6's E2E encryption the same
24//! checks still work — `op_id`/`hlc`/`seq`/`prev` stay cleartext metadata
25//! by design ("the relay can route and dedup on `op_id` and `hlc`").
26//! Device *identity* remains asserted, not authenticated, until B6 signing.
27//!
28//! ## Stable frontier, eviction horizon `H`, and GC
29//!
30//! Straight from the proposal's stragglers section:
31//!
32//! - **Stable frontier** = `min(acked)` over **active (non-evicted) roster
33//!   devices** — `None` (nothing droppable) while any active device has
34//!   never acked, mirroring `compact::AckTable`'s refusal semantics.
35//! - **Eviction**: a device silent (no push/pull/ack) longer than
36//!   [`RelayConfig::eviction_horizon_ms`] is marked
37//!   [`DeviceStatus::Evicted`] on the roster; its ack no longer holds the
38//!   frontier. An evicted device may still push/pull/ack (its re-entry path
39//!   is cold bootstrap — `checkpoint_get` + `pull(since checkpoint
40//!   frontier)` + `resume_anchored`, see `crate::session`), and it is
41//!   **reinstated** when it acks at/above the current stable frontier
42//!   (i.e. it has provably caught up) — or whenever nothing is
43//!   GC-eligible anyway (`stable frontier == None`).
44//! - **GC** ([`Relay::gc`]): an op is droppable only when **both** (a) its
45//!   HLC is at/below the stable frontier AND (b) a stored checkpoint
46//!   **covers** it (`checkpoint.frontier[device].seq >= op.seq`) — "ops
47//!   below the stable frontier are GC-eligible relay-side only after a
48//!   covering checkpoint exists". Both conditions are per-device chain
49//!   *prefixes* (HLC-monotone chains; frontier seqs), so GC always drops a
50//!   prefix and the retained chain stays gap-free; the relay keeps the last
51//!   dropped op's `(seq, op_id, hlc)` as the chain anchor for continuity
52//!   checks and remembers the drop floor so a `pull` whose `since` frontier
53//!   reaches into truncated space fails loudly
54//!   ([`RelayError::FrontierTruncated`]) instead of silently serving a
55//!   gapped log — the signal that sends the puller to cold bootstrap.
56//!
57//! **Deliberate deviation from the proposal, binding on B6:** the proposal's
58//! third `H` rule ("max op age the relay accepts = `H`; older ops are
59//! rejected") exists to keep accepted ops out of *time-truncated* space
60//! (`hlc.wall < now − H`). This relay's truncation is not time-based — it
61//! is per-device-seq + covering-checkpoint, under which a returning
62//! straggler's late ops are structurally *outside* truncated space (its own
63//! chain frontier is behind them) and converge losslessly once pushed,
64//! which is strictly stronger than the proposal's "loses only writes it
65//! made while >H-offline" degenerate case. So B3 accepts old ops rather
66//! than rejecting them. If B6's hosted backend adopts wall-clock-bounded
67//! storage, it must reintroduce the age bound *and* the proposal's
68//! surface-it-to-the-user story together.
69//!
70//! ## Checkpoints
71//!
72//! `checkpoint_put` runs TWO checks, not one. `Checkpoint::verify` proves
73//! internal self-consistency (both hashes recompute) — but that alone lets
74//! a checkpoint built from a *different* chain merely CLAIMING a device's
75//! name walk in and, via the coverage claim, make GC drop that device's
76//! real ops (the kernel-review data-loss defect). So `checkpoint_put` also
77//! **cross-checks the frontier against the relay-held chains**
78//! ([`RelayState::validate_frontier`]): every frontier entry must name an op
79//! the relay actually holds/held for that device at that seq (or, for a
80//! seq already GC'd, be consistent with the remembered dropped head). A
81//! device the relay has never seen a chain for cannot be validated. Only
82//! validated checkpoints are stored, so `gc` — which counts stored
83//! checkpoints as coverage — counts only validated ones, and the
84//! bare-seq coverage test is then sound (a validated frontier head at seq S
85//! means the contiguous hash-linked prefix 0..S is the device's real
86//! chain). `op_id`/`seq`/`prev` stay cleartext metadata under B6 E2E, so
87//! this cross-check survives encryption. Storage **dedups on
88//! `checkpoint_hash` — the whole-record content address, never
89//! `state_hash`** (binding contract from B4: two frontiers can fold to one
90//! state; keying on the state would keep the wrong checkpoint and fork
91//! chains on resume). The "latest" pointer served by `pull`/`checkpoint_get`
92//! advances only to a checkpoint whose frontier **dominates** the current
93//! latest (covers at least every device/seq it covers), so a stale or
94//! concurrent upload can never regress the bootstrap pointer.
95
96use crate::checkpoint::{Checkpoint, CheckpointError, FrontierEntry};
97use crate::oplog::{Hlc, OpRecord, WallClock};
98use serde::{Deserialize, Serialize};
99use std::collections::BTreeMap;
100use std::fmt;
101use std::fs::{self, File, OpenOptions};
102use std::io::Write;
103use std::path::{Path, PathBuf};
104
105/// A pull cursor: device_id → highest `seq` the puller already holds for
106/// that device. Seq-based (not HLC-based) on purpose: a straggler's late
107/// ops carry *old* HLCs but *new* seqs, so a seq frontier still delivers
108/// them to every peer — an HLC cursor would silently skip them.
109pub type Frontier = BTreeMap<String, u64>;
110
111/// The frontier of an op-set: max seq per device.
112pub fn frontier_of(ops: &[OpRecord]) -> Frontier {
113    let mut frontier = Frontier::new();
114    for op in ops {
115        let entry = frontier.entry(op.device_id.clone()).or_insert(op.seq);
116        if op.seq > *entry {
117            *entry = op.seq;
118        }
119    }
120    frontier
121}
122
123/// The pull cursor a checkpoint's coverage corresponds to — the cold
124/// bootstrap's `pull(since = F)`.
125pub fn checkpoint_frontier(checkpoint: &Checkpoint) -> Frontier {
126    checkpoint
127        .frontier
128        .iter()
129        .map(|(device, entry)| (device.clone(), entry.seq))
130        .collect()
131}
132
133/// Roster liveness state — the proposal's "devices+last_seen" with the
134/// eviction verdict made explicit.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum DeviceStatus {
138    Active,
139    /// Silent past the horizon `H`: its ack no longer holds the stable
140    /// frontier; re-entry is cold bootstrap, reinstated on a caught-up ack.
141    Evicted,
142}
143
144/// One device registry entry — `roster()`'s row.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct RosterEntry {
147    pub device_id: String,
148    /// The stamp the device joined at (relay wall reading, counter 0).
149    pub added_at: Hlc,
150    /// Relay wall-clock ms of the device's last push/pull/ack/register.
151    pub last_seen_ms: u64,
152    /// The device's acked fold frontier (monotone-only), `None` until it
153    /// first acks — which holds the stable frontier at `None` (safe).
154    pub acked: Option<Hlc>,
155    pub status: DeviceStatus,
156}
157
158/// Relay policy knobs.
159#[derive(Debug, Clone, Default)]
160pub struct RelayConfig {
161    /// The stragglers horizon `H`: a device silent longer than this is
162    /// marked [`DeviceStatus::Evicted`]. `None` (the default) never evicts
163    /// — the safe, GC-pinning posture a caller must opt out of.
164    pub eviction_horizon_ms: Option<u64>,
165}
166
167/// What a push did. Not transactional: on an error mid-batch the already
168/// accepted prefix stays (retry-safe — a re-push of it dedups).
169#[derive(Debug, Clone, Default, PartialEq, Eq)]
170pub struct PushOutcome {
171    /// Ops newly admitted to the device's relay-held chain.
172    pub accepted: usize,
173    /// Ops the relay already held (retransmission — `op_id` identical) or
174    /// that fall below its GC floor for the device (already covered by a
175    /// checkpoint).
176    pub deduped: usize,
177}
178
179/// What a pull returned — the proposal's `{ops, latest_checkpoint_ptr}`.
180#[derive(Debug, Clone, PartialEq)]
181pub struct PullResult {
182    /// Every retained op above the `since` frontier, in canonical
183    /// `(hlc, op_id)` order.
184    pub ops: Vec<OpRecord>,
185    /// `checkpoint_hash` of the current latest checkpoint, if any — the
186    /// cold-bootstrap pointer.
187    pub latest_checkpoint: Option<String>,
188}
189
190/// What an ack did.
191#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
192pub struct AckOutcome {
193    /// The acked frontier advanced (monotone-only, like `AckTable::ack`).
194    pub advanced: bool,
195    /// An evicted device proved it caught up and is active again.
196    pub reinstated: bool,
197}
198
199/// What GC dropped, per device.
200#[derive(Debug, Clone, Default, PartialEq, Eq)]
201pub struct GcReport {
202    pub dropped: BTreeMap<String, usize>,
203}
204
205impl GcReport {
206    pub fn total(&self) -> usize {
207        self.dropped.values().sum()
208    }
209}
210
211/// A relay operation failure.
212#[derive(Debug)]
213pub enum RelayError {
214    /// A pushed op fails self-verification (id/device mismatch) or does not
215    /// link the relay-held chain head.
216    Chain { device_id: String, detail: String },
217    /// A different op claims a `seq` the relay already holds — the
218    /// permanent chain fork, refused at the transport.
219    Fork { device_id: String, seq: u64 },
220    /// A pushed op skips ahead of the relay-held chain (ops must arrive
221    /// contiguously).
222    Gap { device_id: String, expected: u64, found: u64 },
223    /// A device may only push its own chain.
224    ForeignOps { device_id: String, op_device: String },
225    /// The pull's `since` frontier reaches into GC'd space — the puller
226    /// cannot be served a gapless log and must cold-bootstrap from the
227    /// latest checkpoint (`checkpoint_get` + `pull(since = checkpoint
228    /// frontier)` + `resume_anchored`).
229    FrontierTruncated { device_id: String, dropped_below: u64 },
230    /// The uploaded checkpoint does not verify.
231    Checkpoint(CheckpointError),
232    /// The uploaded checkpoint is internally self-consistent
233    /// ([`Checkpoint::verify`] passed) but its frontier claims a chain
234    /// position that does **not** match the relay-held chain for a device —
235    /// a checkpoint built from a *different* chain merely claiming the
236    /// device's name (a restored-from-backup re-mint, or two accounts
237    /// colliding on a device id in a shared `FsRelay` folder). Rejected so
238    /// it can never become GC coverage evidence and drop the device's real
239    /// ops. (`op_id`/`seq`/`prev` stay cleartext metadata under B6 E2E, so
240    /// this cross-check survives encryption.)
241    CheckpointFrontierUnverified { device_id: String, seq: u64, detail: String },
242    Io(std::io::Error),
243}
244
245impl fmt::Display for RelayError {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            RelayError::Chain { device_id, detail } => {
249                write!(f, "relay push rejected for {device_id}: {detail}")
250            }
251            RelayError::Fork { device_id, seq } => write!(
252                f,
253                "relay push rejected: a different op already holds {device_id} seq {seq} — \
254                 device chain fork"
255            ),
256            RelayError::Gap { device_id, expected, found } => write!(
257                f,
258                "relay push rejected for {device_id}: seq gap (relay expects {expected}, \
259                 got {found}) — push contiguously"
260            ),
261            RelayError::ForeignOps { device_id, op_device } => write!(
262                f,
263                "relay push rejected: device {device_id} pushed an op emitted by {op_device} — \
264                 a device pushes only its own chain"
265            ),
266            RelayError::FrontierTruncated { device_id, dropped_below } => write!(
267                f,
268                "pull frontier reaches into GC'd space (device {device_id}: ops below seq \
269                 {dropped_below} were truncated) — cold-bootstrap from the latest checkpoint"
270            ),
271            RelayError::Checkpoint(e) => write!(f, "relay checkpoint rejected: {e}"),
272            RelayError::CheckpointFrontierUnverified { device_id, seq, detail } => write!(
273                f,
274                "relay checkpoint rejected: frontier for device {device_id} at seq {seq} does \
275                 not match the relay-held chain ({detail}) — forged or foreign checkpoint, \
276                 refusing to store it (it would become GC coverage for ops it does not cover)"
277            ),
278            RelayError::Io(e) => write!(f, "relay io error: {e}"),
279        }
280    }
281}
282
283impl std::error::Error for RelayError {}
284
285/// The relay contract — see the module docs. All methods take `&mut self`
286/// because every contact updates roster liveness (and runs the eviction
287/// sweep) even on logically-read-only calls.
288pub trait Relay {
289    /// Enroll (or touch) a device on the roster. Idempotent; push/pull/ack
290    /// auto-register on first contact.
291    fn register(&mut self, device_id: &str) -> Result<RosterEntry, RelayError>;
292    /// Admit `device_id`'s own journal-durable ops (in seq order) onto its
293    /// relay-held chain. Contract (B1, binding): the caller transmits only
294    /// ops that are already journal-durable on the device.
295    fn push(&mut self, device_id: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError>;
296    /// Every retained op above `since` (per-device seq cursor) + the latest
297    /// checkpoint pointer.
298    fn pull(&mut self, device_id: &str, since: &Frontier) -> Result<PullResult, RelayError>;
299    /// Record a device's fold frontier. Contract (B4, binding): the device
300    /// acks only what it has DURABLY folded (journaled), never merely
301    /// received.
302    fn ack(&mut self, device_id: &str, frontier: Hlc) -> Result<AckOutcome, RelayError>;
303    /// Store a device-computed checkpoint. Verified, deduped on
304    /// `checkpoint_hash` (whole-record content address — contract from B4).
305    /// Returns `false` when the identical checkpoint was already stored.
306    fn checkpoint_put(
307        &mut self,
308        device_id: &str,
309        checkpoint: &Checkpoint,
310    ) -> Result<bool, RelayError>;
311    /// The latest stored checkpoint (dominance-monotone pointer), if any.
312    fn checkpoint_get(&mut self) -> Result<Option<Checkpoint>, RelayError>;
313    /// The device registry.
314    fn roster(&mut self) -> Result<Vec<RosterEntry>, RelayError>;
315    /// `min(acked)` over active roster devices; `None` while any active
316    /// device has never acked (nothing is droppable then).
317    fn stable_frontier(&mut self) -> Result<Option<Hlc>, RelayError>;
318    /// Drop every op that is BOTH at/below the stable frontier AND covered
319    /// by a stored checkpoint. Never drops anything else.
320    fn gc(&mut self) -> Result<GcReport, RelayError>;
321}
322
323/// One device's relay-held chain.
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325struct DeviceChain {
326    /// Retained ops by seq (contiguous by construction).
327    ops: BTreeMap<u64, OpRecord>,
328    /// Seqs below this were GC'd (0 = nothing dropped).
329    dropped_below: u64,
330    /// The last GC'd op's `(seq, op_id, hlc)` — the chain-continuity anchor
331    /// for the first retained/pushed op after a GC.
332    dropped_head: Option<(u64, String, Hlc)>,
333}
334
335impl DeviceChain {
336    /// The chain head the next pushed op must link: the last retained op,
337    /// else the last GC'd op.
338    fn head(&self) -> Option<(u64, &str, &Hlc)> {
339        self.ops
340            .iter()
341            .next_back()
342            .map(|(seq, op)| (*seq, op.op_id.as_str(), &op.hlc))
343            .or_else(|| {
344                self.dropped_head
345                    .as_ref()
346                    .map(|(seq, id, hlc)| (*seq, id.as_str(), hlc))
347            })
348    }
349
350    fn next_seq(&self) -> u64 {
351        self.head().map(|(seq, _, _)| seq + 1).unwrap_or(0)
352    }
353}
354
355/// The relay's pure state machine — shared verbatim by [`InMemoryRelay`]
356/// (held in memory) and [`FsRelay`] (loaded/persisted around every call).
357#[derive(Debug, Default, Serialize, Deserialize)]
358struct RelayState {
359    roster: BTreeMap<String, RosterEntry>,
360    chains: BTreeMap<String, DeviceChain>,
361    /// Content-addressed checkpoint store (dedup key = `checkpoint_hash`).
362    /// `FsRelay` persists these as their own `<hash>.checkpoint.json`
363    /// files (re-verified on load), not inside the state file.
364    #[serde(skip)]
365    checkpoints: BTreeMap<String, Checkpoint>,
366    /// `checkpoint_hash` of the dominance-latest checkpoint.
367    latest_checkpoint: Option<String>,
368}
369
370/// Does checkpoint `a`'s frontier dominate `b`'s (cover at least every
371/// device/seq `b` covers)? The relay's latest-pointer advance rule and the
372/// session's rebase-regression guard.
373pub(crate) fn frontier_dominates(a: &Checkpoint, b: &Checkpoint) -> bool {
374    b.frontier.iter().all(|(device, entry)| {
375        a.frontier
376            .get(device)
377            .is_some_and(|ae: &FrontierEntry| ae.seq >= entry.seq)
378    })
379}
380
381impl RelayState {
382    /// Touch (or enroll) a device and run the eviction sweep. Order
383    /// matters: the caller is touched first so it can never evict itself
384    /// mid-call.
385    fn touch_and_sweep(&mut self, device_id: &str, now_ms: u64, config: &RelayConfig) {
386        let entry = self
387            .roster
388            .entry(device_id.to_string())
389            .or_insert_with(|| RosterEntry {
390                device_id: device_id.to_string(),
391                added_at: Hlc { wall_ms: now_ms, counter: 0, device_id: device_id.to_string() },
392                last_seen_ms: now_ms,
393                acked: None,
394                status: DeviceStatus::Active,
395            });
396        entry.last_seen_ms = now_ms;
397        if let Some(horizon) = config.eviction_horizon_ms {
398            for entry in self.roster.values_mut() {
399                if entry.status == DeviceStatus::Active
400                    && now_ms.saturating_sub(entry.last_seen_ms) > horizon
401                {
402                    entry.status = DeviceStatus::Evicted;
403                }
404            }
405        }
406    }
407
408    fn stable_frontier(&self) -> Option<Hlc> {
409        let active: Vec<&RosterEntry> = self
410            .roster
411            .values()
412            .filter(|e| e.status == DeviceStatus::Active)
413            .collect();
414        if active.is_empty() || active.iter().any(|e| e.acked.is_none()) {
415            return None;
416        }
417        active.iter().filter_map(|e| e.acked.clone()).min()
418    }
419
420    fn push(&mut self, device_id: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError> {
421        let mut outcome = PushOutcome::default();
422        // Process in seq order regardless of slice order.
423        let mut sorted: Vec<&OpRecord> = ops.iter().collect();
424        sorted.sort_by_key(|op| op.seq);
425        for op in sorted {
426            if op.device_id != device_id {
427                return Err(RelayError::ForeignOps {
428                    device_id: device_id.to_string(),
429                    op_device: op.device_id.clone(),
430                });
431            }
432            if !op.id_valid() {
433                return Err(RelayError::Chain {
434                    device_id: device_id.to_string(),
435                    detail: format!("op {}: stored op_id does not match content", op.op_id),
436                });
437            }
438            if op.hlc.device_id != op.device_id {
439                return Err(RelayError::Chain {
440                    device_id: device_id.to_string(),
441                    detail: format!("op {}: hlc.device_id != device_id", op.op_id),
442                });
443            }
444            let chain = self.chains.entry(device_id.to_string()).or_default();
445            if op.seq < chain.dropped_below {
446                // The GC'd prefix: a device re-pushing its own already
447                // checkpoint-covered ops (crash lost its push cursor).
448                outcome.deduped += 1;
449                continue;
450            }
451            if let Some(existing) = chain.ops.get(&op.seq) {
452                if existing.op_id == op.op_id {
453                    outcome.deduped += 1;
454                    continue;
455                }
456                return Err(RelayError::Fork { device_id: device_id.to_string(), seq: op.seq });
457            }
458            let expected = chain.next_seq();
459            if op.seq != expected {
460                return Err(RelayError::Gap {
461                    device_id: device_id.to_string(),
462                    expected,
463                    found: op.seq,
464                });
465            }
466            match chain.head() {
467                Some((_, head_id, head_hlc)) => {
468                    if op.prev.as_deref() != Some(head_id) {
469                        return Err(RelayError::Chain {
470                            device_id: device_id.to_string(),
471                            detail: format!(
472                                "op {}: prev does not link the relay-held head {head_id}",
473                                op.op_id
474                            ),
475                        });
476                    }
477                    if op.hlc <= *head_hlc {
478                        return Err(RelayError::Chain {
479                            device_id: device_id.to_string(),
480                            detail: format!(
481                                "op {}: hlc does not advance past the relay-held head",
482                                op.op_id
483                            ),
484                        });
485                    }
486                }
487                None => {
488                    if op.prev.is_some() {
489                        return Err(RelayError::Chain {
490                            device_id: device_id.to_string(),
491                            detail: format!("op {}: seq 0 must have no prev", op.op_id),
492                        });
493                    }
494                }
495            }
496            chain.ops.insert(op.seq, op.clone());
497            outcome.accepted += 1;
498        }
499        Ok(outcome)
500    }
501
502    fn pull(&self, since: &Frontier) -> Result<PullResult, RelayError> {
503        let mut ops = Vec::new();
504        for (device_id, chain) in &self.chains {
505            let start = since.get(device_id).map(|held| held + 1).unwrap_or(0);
506            if start < chain.dropped_below {
507                return Err(RelayError::FrontierTruncated {
508                    device_id: device_id.clone(),
509                    dropped_below: chain.dropped_below,
510                });
511            }
512            ops.extend(chain.ops.range(start..).map(|(_, op)| op.clone()));
513        }
514        ops.sort_by(|a, b| (&a.hlc, &a.op_id).cmp(&(&b.hlc, &b.op_id)));
515        Ok(PullResult { ops, latest_checkpoint: self.latest_checkpoint.clone() })
516    }
517
518    fn ack(&mut self, device_id: &str, frontier: Hlc) -> AckOutcome {
519        // Frontier over the OTHER active devices — the bar a returning
520        // straggler must clear to be reinstated.
521        let others_frontier = {
522            let others: Vec<&RosterEntry> = self
523                .roster
524                .values()
525                .filter(|e| e.status == DeviceStatus::Active && e.device_id != device_id)
526                .collect();
527            if others.is_empty() || others.iter().any(|e| e.acked.is_none()) {
528                None
529            } else {
530                others.iter().filter_map(|e| e.acked.clone()).min()
531            }
532        };
533        let entry = self.roster.get_mut(device_id).expect("touched before ack");
534        let advanced = match &entry.acked {
535            Some(current) if frontier <= *current => false,
536            _ => {
537                entry.acked = Some(frontier);
538                true
539            }
540        };
541        let mut reinstated = false;
542        if entry.status == DeviceStatus::Evicted {
543            // Reinstate when the device has provably caught up: its acked
544            // frontier reaches the active stable frontier (or nothing is
545            // GC-eligible anyway). Its stale ack must never drag GC back.
546            let caught_up = match (&entry.acked, &others_frontier) {
547                (Some(acked), Some(frontier)) => acked >= frontier,
548                (Some(_), None) => true,
549                (None, _) => false,
550            };
551            if caught_up {
552                entry.status = DeviceStatus::Active;
553                reinstated = true;
554            }
555        }
556        AckOutcome { advanced, reinstated }
557    }
558
559    /// Cross-check a checkpoint's frontier against the relay-held chains —
560    /// the defense `Checkpoint::verify` (internal self-consistency) cannot
561    /// give. Every frontier entry must name an op the relay actually
562    /// holds/held for that device at that seq: the whole prefix a checkpoint
563    /// claims to cover has to be the device's REAL chain, or GC would drop
564    /// real ops on a forged coverage claim (kernel-review data-loss defect).
565    /// A device the relay has never seen a chain for cannot be validated.
566    fn validate_frontier(&self, checkpoint: &Checkpoint) -> Result<(), RelayError> {
567        for (device_id, entry) in &checkpoint.frontier {
568            let unverified = |detail: &str| RelayError::CheckpointFrontierUnverified {
569                device_id: device_id.clone(),
570                seq: entry.seq,
571                detail: detail.to_string(),
572            };
573            let Some(chain) = self.chains.get(device_id) else {
574                return Err(unverified("relay holds no chain for this device"));
575            };
576            if entry.seq >= chain.dropped_below {
577                // Still-retained region: the relay must hold exactly this op.
578                match chain.ops.get(&entry.seq) {
579                    Some(op) if op.op_id == entry.head => {}
580                    Some(_) => {
581                        return Err(unverified(
582                            "frontier head does not match the relay-held op at this seq",
583                        ))
584                    }
585                    None => {
586                        return Err(unverified(
587                            "relay holds no op at the claimed frontier seq (claims coverage \
588                             beyond its chain head)",
589                        ))
590                    }
591                }
592            } else {
593                // Already-GC'd region: verify against the remembered dropped
594                // head where the seq lands on it; a deeper seq was truncated
595                // by a prior *validated* checkpoint, so accept it (it covers
596                // only already-dropped ops — no new coverage, no new risk).
597                match &chain.dropped_head {
598                    Some((seq, id, _)) if *seq == entry.seq => {
599                        if id != &entry.head {
600                            return Err(unverified(
601                                "frontier head does not match the relay's GC'd dropped head",
602                            ));
603                        }
604                    }
605                    Some((seq, _, _)) if entry.seq < *seq => {}
606                    _ => {
607                        return Err(unverified(
608                            "claimed frontier seq is below the relay's GC floor with no \
609                             matching record",
610                        ))
611                    }
612                }
613            }
614        }
615        Ok(())
616    }
617
618    fn checkpoint_put(&mut self, checkpoint: &Checkpoint) -> Result<bool, RelayError> {
619        checkpoint.verify().map_err(RelayError::Checkpoint)?;
620        // Cross-check the frontier against the relay-held chains BEFORE the
621        // checkpoint can become GC coverage — self-consistency (verify) is
622        // not enough (kernel-review data-loss defect). Only validated
623        // checkpoints enter the store, so `gc` counting stored checkpoints
624        // as coverage counts only validated ones.
625        self.validate_frontier(checkpoint)?;
626        // Dedup on the WHOLE-RECORD content address, never state_hash (B4
627        // contract: two frontiers can fold to one state).
628        let stored = if self.checkpoints.contains_key(&checkpoint.checkpoint_hash) {
629            false
630        } else {
631            self.checkpoints
632                .insert(checkpoint.checkpoint_hash.clone(), checkpoint.clone());
633            true
634        };
635        let advance = match self
636            .latest_checkpoint
637            .as_ref()
638            .and_then(|hash| self.checkpoints.get(hash))
639        {
640            Some(current) => {
641                checkpoint.checkpoint_hash != current.checkpoint_hash
642                    && frontier_dominates(checkpoint, current)
643            }
644            None => true,
645        };
646        if advance {
647            self.latest_checkpoint = Some(checkpoint.checkpoint_hash.clone());
648        }
649        Ok(stored)
650    }
651
652    fn checkpoint_get(&self) -> Option<Checkpoint> {
653        self.latest_checkpoint
654            .as_ref()
655            .and_then(|hash| self.checkpoints.get(hash))
656            .cloned()
657    }
658
659    fn gc(&mut self) -> GcReport {
660        let mut report = GcReport::default();
661        let Some(frontier) = self.stable_frontier() else {
662            return report;
663        };
664        // Max covered seq per device across all (validated — every stored
665        // checkpoint passed `validate_frontier`) checkpoints, computed once:
666        // O(checkpoints × devices), not O(ops × checkpoints) per device.
667        let mut max_covered: BTreeMap<String, u64> = BTreeMap::new();
668        for ckpt in self.checkpoints.values() {
669            for (device, entry) in &ckpt.frontier {
670                let slot = max_covered.entry(device.clone()).or_insert(entry.seq);
671                if entry.seq > *slot {
672                    *slot = entry.seq;
673                }
674            }
675        }
676        for (device_id, chain) in &mut self.chains {
677            // Both droppability conditions are chain prefixes; walk from the
678            // bottom and stop at the first op failing either.
679            let covered_through = max_covered.get(device_id).copied();
680            let mut droppable: Vec<u64> = Vec::new();
681            for (seq, op) in &chain.ops {
682                let below_frontier = op.hlc <= frontier;
683                let covered = covered_through.is_some_and(|through| through >= *seq);
684                if below_frontier && covered {
685                    droppable.push(*seq);
686                } else {
687                    break;
688                }
689            }
690            for seq in &droppable {
691                let op = chain.ops.remove(seq).expect("collected from the map");
692                chain.dropped_below = seq + 1;
693                chain.dropped_head = Some((*seq, op.op_id, op.hlc));
694            }
695            if !droppable.is_empty() {
696                report.dropped.insert(device_id.clone(), droppable.len());
697            }
698        }
699        report
700    }
701}
702
703/// The in-process reference relay: [`RelayState`] + an injected wall clock.
704pub struct InMemoryRelay {
705    state: RelayState,
706    config: RelayConfig,
707    wall: WallClock,
708}
709
710impl fmt::Debug for InMemoryRelay {
711    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
712        f.debug_struct("InMemoryRelay")
713            .field("state", &self.state)
714            .field("config", &self.config)
715            .finish_non_exhaustive()
716    }
717}
718
719impl InMemoryRelay {
720    pub fn new(config: RelayConfig, wall: WallClock) -> Self {
721        Self { state: RelayState::default(), config, wall }
722    }
723}
724
725impl Relay for InMemoryRelay {
726    fn register(&mut self, device_id: &str) -> Result<RosterEntry, RelayError> {
727        let now = (self.wall)();
728        self.state.touch_and_sweep(device_id, now, &self.config);
729        Ok(self.state.roster[device_id].clone())
730    }
731
732    fn push(&mut self, device_id: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError> {
733        let now = (self.wall)();
734        self.state.touch_and_sweep(device_id, now, &self.config);
735        self.state.push(device_id, ops)
736    }
737
738    fn pull(&mut self, device_id: &str, since: &Frontier) -> Result<PullResult, RelayError> {
739        let now = (self.wall)();
740        self.state.touch_and_sweep(device_id, now, &self.config);
741        self.state.pull(since)
742    }
743
744    fn ack(&mut self, device_id: &str, frontier: Hlc) -> Result<AckOutcome, RelayError> {
745        let now = (self.wall)();
746        self.state.touch_and_sweep(device_id, now, &self.config);
747        Ok(self.state.ack(device_id, frontier))
748    }
749
750    fn checkpoint_put(
751        &mut self,
752        device_id: &str,
753        checkpoint: &Checkpoint,
754    ) -> Result<bool, RelayError> {
755        let now = (self.wall)();
756        self.state.touch_and_sweep(device_id, now, &self.config);
757        self.state.checkpoint_put(checkpoint)
758    }
759
760    fn checkpoint_get(&mut self) -> Result<Option<Checkpoint>, RelayError> {
761        Ok(self.state.checkpoint_get())
762    }
763
764    fn roster(&mut self) -> Result<Vec<RosterEntry>, RelayError> {
765        Ok(self.state.roster.values().cloned().collect())
766    }
767
768    fn stable_frontier(&mut self) -> Result<Option<Hlc>, RelayError> {
769        Ok(self.state.stable_frontier())
770    }
771
772    fn gc(&mut self) -> Result<GcReport, RelayError> {
773        Ok(self.state.gc())
774    }
775}
776
777/// The filesystem loopback relay: two (or more) `DeviceLog`s syncing
778/// through a **shared directory** — the realistic single-user two-Mac case
779/// (a shared volume, an external disk, a user-managed synced folder).
780///
781/// Layout under `dir`:
782/// - `relay.lock` — exclusive advisory lock held around every call (the
783///   `OplogJournal`/`car-registry` protocol, but *blocking*: concurrent
784///   callers queue rather than fail);
785/// - `relay-state.json` — roster + chains + latest-checkpoint pointer
786///   (temp + atomic rename per mutation);
787/// - `checkpoints/<checkpoint_hash>.checkpoint.json` — content-addressed
788///   checkpoint files via `Checkpoint::save`/`load`. Checkpoint files are
789///   **immutable** (the name IS the whole-record content address), so each
790///   is `Checkpoint::load`-verified **once** — on first sight by this
791///   handle — and then served from an in-memory cache; it is never
792///   re-hashed or re-`fsync`'d on subsequent (including read-only) calls.
793///   A tampering attacker in the shared folder is caught by the *next
794///   process* to open the relay (a fresh handle with a cold cache
795///   re-verifies), which is the actual two-Mac threat model; a handle
796///   never serves content it did not verify.
797///
798/// Semantics are identical to [`InMemoryRelay`] by construction — both
799/// drive the same [`RelayState`] core; this type only adds durability and
800/// cross-process mutual exclusion.
801pub struct FsRelay {
802    dir: PathBuf,
803    config: RelayConfig,
804    wall: WallClock,
805    /// Verified-once checkpoint cache (hash → checkpoint). Immutable
806    /// content-addressed files never need re-verification within a handle's
807    /// lifetime — the fix for O(all checkpoints × state) re-hash per call.
808    checkpoint_cache: BTreeMap<String, Checkpoint>,
809}
810
811impl fmt::Debug for FsRelay {
812    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813        f.debug_struct("FsRelay")
814            .field("dir", &self.dir)
815            .field("config", &self.config)
816            .finish_non_exhaustive()
817    }
818}
819
820impl FsRelay {
821    pub fn open(dir: &Path, config: RelayConfig, wall: WallClock) -> std::io::Result<Self> {
822        fs::create_dir_all(dir.join("checkpoints"))?;
823        Ok(Self {
824            dir: dir.to_path_buf(),
825            config,
826            wall,
827            checkpoint_cache: BTreeMap::new(),
828        })
829    }
830
831    fn state_path(&self) -> PathBuf {
832        self.dir.join("relay-state.json")
833    }
834
835    fn checkpoints_dir(&self) -> PathBuf {
836        self.dir.join("checkpoints")
837    }
838
839    /// Run `f` over the loaded state under the exclusive lock, then persist
840    /// — even when `f` returns a business error (`push` keeps its accepted
841    /// prefix, matching the in-memory semantics).
842    fn with_state<T>(
843        &mut self,
844        f: impl FnOnce(&mut RelayState, u64, &RelayConfig) -> Result<T, RelayError>,
845    ) -> Result<T, RelayError> {
846        let lock = OpenOptions::new()
847            .read(true)
848            .write(true)
849            .create(true)
850            .truncate(false)
851            .open(self.dir.join("relay.lock"))
852            .map_err(RelayError::Io)?;
853        lock.lock().map_err(RelayError::Io)?; // blocking; released on drop
854
855        let mut state: RelayState = match fs::read_to_string(self.state_path()) {
856            Ok(raw) => serde_json::from_str(&raw)
857                .map_err(|e| RelayError::Io(std::io::Error::other(e)))?,
858            Err(e) if e.kind() == std::io::ErrorKind::NotFound => RelayState::default(),
859            Err(e) => return Err(RelayError::Io(e)),
860        };
861        // Checkpoint files are immutable content addresses: `Checkpoint::load`
862        // (re-verify both hashes + the file-name address) runs ONCE per
863        // file, on first sight by this handle; a cache hit serves the
864        // already-verified record without re-hashing the whole state.
865        for entry in fs::read_dir(self.checkpoints_dir()).map_err(RelayError::Io)? {
866            let path = entry.map_err(RelayError::Io)?.path();
867            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
868                continue;
869            };
870            let Some(hash) = name.strip_suffix(".checkpoint.json") else {
871                continue;
872            };
873            let ckpt = match self.checkpoint_cache.get(hash) {
874                Some(cached) => cached.clone(),
875                None => {
876                    let ckpt = Checkpoint::load(&path).map_err(RelayError::Checkpoint)?;
877                    self.checkpoint_cache
878                        .insert(ckpt.checkpoint_hash.clone(), ckpt.clone());
879                    ckpt
880                }
881            };
882            state.checkpoints.insert(ckpt.checkpoint_hash.clone(), ckpt);
883        }
884
885        let now = (self.wall)();
886        let result = f(&mut state, now, &self.config);
887
888        // Persist: checkpoint files first (content-addressed, immutable),
889        // then the state file naming them (temp + atomic rename) — the same
890        // durable-referent-first ordering as compact_and_truncate. Skip the
891        // fsync-heavy `save` for any checkpoint already on disk (the name IS
892        // the content, so an existing file is byte-identical) — the fix for
893        // re-fsyncing every checkpoint on every call, including read-only
894        // ones.
895        for ckpt in state.checkpoints.values() {
896            let path = self.checkpoints_dir().join(ckpt.file_name());
897            if !path.exists() {
898                ckpt.save(&self.checkpoints_dir()).map_err(RelayError::Io)?;
899            }
900            self.checkpoint_cache
901                .entry(ckpt.checkpoint_hash.clone())
902                .or_insert_with(|| ckpt.clone());
903        }
904        let tmp = self.dir.join("relay-state.json.tmp");
905        {
906            let mut file = File::create(&tmp).map_err(RelayError::Io)?;
907            file.write_all(
908                serde_json::to_string(&state)
909                    .map_err(|e| RelayError::Io(std::io::Error::other(e)))?
910                    .as_bytes(),
911            )
912            .map_err(RelayError::Io)?;
913            file.sync_all().map_err(RelayError::Io)?;
914        }
915        fs::rename(&tmp, self.state_path()).map_err(RelayError::Io)?;
916        result
917    }
918}
919
920impl Relay for FsRelay {
921    fn register(&mut self, device_id: &str) -> Result<RosterEntry, RelayError> {
922        self.with_state(|state, now, config| {
923            state.touch_and_sweep(device_id, now, config);
924            Ok(state.roster[device_id].clone())
925        })
926    }
927
928    fn push(&mut self, device_id: &str, ops: &[OpRecord]) -> Result<PushOutcome, RelayError> {
929        self.with_state(|state, now, config| {
930            state.touch_and_sweep(device_id, now, config);
931            state.push(device_id, ops)
932        })
933    }
934
935    fn pull(&mut self, device_id: &str, since: &Frontier) -> Result<PullResult, RelayError> {
936        self.with_state(|state, now, config| {
937            state.touch_and_sweep(device_id, now, config);
938            state.pull(since)
939        })
940    }
941
942    fn ack(&mut self, device_id: &str, frontier: Hlc) -> Result<AckOutcome, RelayError> {
943        self.with_state(|state, now, config| {
944            state.touch_and_sweep(device_id, now, config);
945            Ok(state.ack(device_id, frontier))
946        })
947    }
948
949    fn checkpoint_put(
950        &mut self,
951        device_id: &str,
952        checkpoint: &Checkpoint,
953    ) -> Result<bool, RelayError> {
954        self.with_state(|state, now, config| {
955            state.touch_and_sweep(device_id, now, config);
956            state.checkpoint_put(checkpoint)
957        })
958    }
959
960    fn checkpoint_get(&mut self) -> Result<Option<Checkpoint>, RelayError> {
961        self.with_state(|state, _, _| Ok(state.checkpoint_get()))
962    }
963
964    fn roster(&mut self) -> Result<Vec<RosterEntry>, RelayError> {
965        self.with_state(|state, _, _| Ok(state.roster.values().cloned().collect()))
966    }
967
968    fn stable_frontier(&mut self) -> Result<Option<Hlc>, RelayError> {
969        self.with_state(|state, _, _| Ok(state.stable_frontier()))
970    }
971
972    fn gc(&mut self) -> Result<GcReport, RelayError> {
973        self.with_state(|state, _, _| Ok(state.gc()))
974    }
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980    use crate::oplog::{DeviceLog, Scope, Surface};
981    use std::sync::atomic::{AtomicU64, Ordering};
982    use std::sync::Arc;
983
984    fn manual_clock() -> (Arc<AtomicU64>, WallClock) {
985        let t = Arc::new(AtomicU64::new(0));
986        let reader = t.clone();
987        (t, Arc::new(move || reader.load(Ordering::SeqCst)))
988    }
989
990    fn mem_relay() -> InMemoryRelay {
991        InMemoryRelay::new(RelayConfig::default(), Arc::new(|| 0))
992    }
993
994    fn ops_for(device: &str, n: usize) -> (DeviceLog, Vec<OpRecord>) {
995        let mut log = DeviceLog::new(device);
996        let ops = (0..n)
997            .map(|i| {
998                log.append(
999                    Scope::Personal,
1000                    Surface::Knowledge,
1001                    serde_json::json!({"id": format!("{device}-f{i}")}),
1002                )
1003            })
1004            .collect();
1005        (log, ops)
1006    }
1007
1008    #[test]
1009    fn push_validates_the_chain_and_dedups_retransmission() {
1010        let mut relay = mem_relay();
1011        let (mut log, ops) = ops_for("a", 3);
1012
1013        let outcome = relay.push("a", &ops).unwrap();
1014        assert_eq!(outcome, PushOutcome { accepted: 3, deduped: 0 });
1015
1016        // Retransmission (crash lost the push cursor): pure dedup.
1017        let again = relay.push("a", &ops).unwrap();
1018        assert_eq!(again, PushOutcome { accepted: 0, deduped: 3 });
1019
1020        // Continuation accepted.
1021        let next = log.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "x"}));
1022        assert_eq!(relay.push("a", &[next]).unwrap().accepted, 1);
1023
1024        // A gap (skipping a seq) is rejected.
1025        log.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "skipped"}));
1026        let ahead = log.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "y"}));
1027        assert!(matches!(
1028            relay.push("a", &[ahead]),
1029            Err(RelayError::Gap { expected: 4, found: 5, .. })
1030        ));
1031
1032        // A fork (different op at a held seq) is rejected.
1033        let mut forked = DeviceLog::new("a");
1034        let f0 = forked.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "evil"}));
1035        assert!(matches!(relay.push("a", &[f0]), Err(RelayError::Fork { seq: 0, .. })));
1036
1037        // Foreign ops are rejected.
1038        let (_, b_ops) = ops_for("b", 1);
1039        assert!(matches!(
1040            relay.push("a", &b_ops),
1041            Err(RelayError::ForeignOps { .. })
1042        ));
1043
1044        // A tampered op is rejected.
1045        let mut tampered = ops[0].clone();
1046        tampered.payload = serde_json::json!({"forged": true});
1047        assert!(matches!(relay.push("b", &[tampered]), Err(RelayError::ForeignOps { .. })));
1048        let mut own_tampered = ops[0].clone();
1049        own_tampered.payload = serde_json::json!({"id": "a-f0", "forged": true});
1050        assert!(matches!(relay.push("a", &[own_tampered]), Err(RelayError::Chain { .. })));
1051    }
1052
1053    #[test]
1054    fn pull_is_a_seq_cursor_and_serves_canonical_order() {
1055        let mut relay = mem_relay();
1056        let (_, a_ops) = ops_for("a", 3);
1057        let (_, b_ops) = ops_for("b", 2);
1058        relay.push("a", &a_ops).unwrap();
1059        relay.push("b", &b_ops).unwrap();
1060
1061        // Fresh puller: everything.
1062        let all = relay.pull("c", &Frontier::new()).unwrap();
1063        assert_eq!(all.ops.len(), 5);
1064        assert!(all.latest_checkpoint.is_none());
1065
1066        // Cursor past a's seq 1 and all of b: only a's tail comes back.
1067        let mut since = Frontier::new();
1068        since.insert("a".into(), 1);
1069        since.insert("b".into(), 1);
1070        let tail = relay.pull("c", &since).unwrap();
1071        assert_eq!(tail.ops.len(), 1);
1072        assert_eq!(tail.ops[0].seq, 2);
1073        assert_eq!(tail.ops[0].device_id, "a");
1074    }
1075
1076    #[test]
1077    fn stable_frontier_requires_every_active_device_acked() {
1078        let mut relay = mem_relay();
1079        let (_, a_ops) = ops_for("a", 2);
1080        relay.push("a", &a_ops).unwrap();
1081        relay.register("b").unwrap();
1082
1083        // b never acked → no frontier (nothing droppable) — the AckTable
1084        // refusal semantics, relay-side.
1085        relay.ack("a", a_ops[1].hlc.clone()).unwrap();
1086        assert_eq!(relay.stable_frontier().unwrap(), None);
1087
1088        relay.ack("b", a_ops[0].hlc.clone()).unwrap();
1089        assert_eq!(relay.stable_frontier().unwrap(), Some(a_ops[0].hlc.clone()), "min(acked)");
1090
1091        // Monotone-only ack: a replayed lower ack cannot regress it.
1092        let outcome = relay.ack("b", a_ops[0].hlc.clone()).unwrap();
1093        assert!(!outcome.advanced);
1094        let outcome = relay.ack("b", a_ops[1].hlc.clone()).unwrap();
1095        assert!(outcome.advanced);
1096        assert_eq!(relay.stable_frontier().unwrap(), Some(a_ops[1].hlc.clone()));
1097    }
1098
1099    #[test]
1100    fn gc_requires_both_frontier_and_covering_checkpoint() {
1101        let mut relay = mem_relay();
1102        let (_, ops) = ops_for("a", 4);
1103        relay.push("a", &ops).unwrap();
1104        relay.ack("a", ops[3].hlc.clone()).unwrap();
1105
1106        // Everything is below the stable frontier, but NO covering
1107        // checkpoint exists → nothing drops.
1108        assert_eq!(relay.gc().unwrap().total(), 0);
1109
1110        // Checkpoint covering the first two ops → exactly those drop.
1111        let ckpt = Checkpoint::from_ops(&ops[..2]).unwrap();
1112        assert!(relay.checkpoint_put("a", &ckpt).unwrap());
1113        let report = relay.gc().unwrap();
1114        assert_eq!(report.dropped["a"], 2);
1115
1116        // Ops above the frontier never drop: push more, don't ack.
1117        let mut log = DeviceLog::resume("a", &ops).unwrap();
1118        let newer = log.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "n"}));
1119        relay.push("a", std::slice::from_ref(&newer)).unwrap();
1120        let full_ckpt = {
1121            let mut all = ops.clone();
1122            all.push(newer);
1123            Checkpoint::from_ops(&all).unwrap()
1124        };
1125        relay.checkpoint_put("a", &full_ckpt).unwrap();
1126        // Covered by a checkpoint, but seq 4 is above the acked frontier →
1127        // only seqs 2..=3 drop.
1128        let report = relay.gc().unwrap();
1129        assert_eq!(report.dropped["a"], 2);
1130        let survivors = relay.pull("b", &Frontier::new());
1131        // A fresh pull now reaches into truncated space → cold bootstrap.
1132        assert!(matches!(
1133            survivors,
1134            Err(RelayError::FrontierTruncated { dropped_below: 4, .. })
1135        ));
1136        // But a caught-up cursor is served the retained tail.
1137        let mut since = Frontier::new();
1138        since.insert("a".into(), 3);
1139        assert_eq!(relay.pull("b", &since).unwrap().ops.len(), 1);
1140    }
1141
1142    #[test]
1143    fn gc_preserves_chain_continuity_for_later_pushes() {
1144        let mut relay = mem_relay();
1145        let (mut log, ops) = ops_for("a", 3);
1146        relay.push("a", &ops).unwrap();
1147        relay.ack("a", ops[2].hlc.clone()).unwrap();
1148        let ckpt = Checkpoint::from_ops(&ops).unwrap();
1149        relay.checkpoint_put("a", &ckpt).unwrap();
1150        assert_eq!(relay.gc().unwrap().dropped["a"], 3);
1151
1152        // The whole chain is GC'd; a continuation still push-verifies
1153        // against the remembered dropped head…
1154        let next = log.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "n"}));
1155        assert_eq!(relay.push("a", &[next]).unwrap().accepted, 1);
1156        // …and a re-push of the GC'd prefix dedups instead of forking.
1157        assert_eq!(relay.push("a", &ops).unwrap(), PushOutcome { accepted: 0, deduped: 3 });
1158    }
1159
1160    #[test]
1161    fn forged_checkpoint_frontier_is_rejected_and_never_becomes_gc_coverage() {
1162        // Kernel-review DATA-LOSS repro (through the public API): a
1163        // self-consistent checkpoint built from a DIFFERENT chain that
1164        // merely CLAIMS device "a"'s name must not walk in via checkpoint_put
1165        // (verify() only checks internal hashes) and let GC drop a's REAL
1166        // ops on the coverage claim.
1167        let mut relay = mem_relay();
1168
1169        // a's REAL chain, pushed + acked.
1170        let mut a = DeviceLog::new("a");
1171        let real: Vec<OpRecord> = (0..3)
1172            .map(|i| {
1173                a.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": format!("real-{i}")}))
1174            })
1175            .collect();
1176        relay.push("a", &real).unwrap();
1177        relay.ack("a", real[2].hlc.clone()).unwrap();
1178
1179        // A DIFFERENT chain claiming device_id "a" (another account's shared
1180        // relay dir, a restored-from-backup re-mint). Internally valid.
1181        let mut fake = DeviceLog::new("a");
1182        let other: Vec<OpRecord> = (0..3)
1183            .map(|i| {
1184                fake.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": format!("other-{i}")}))
1185            })
1186            .collect();
1187        let forged = Checkpoint::from_ops(&other).unwrap();
1188
1189        // The relay REJECTS it — its frontier head does not match the
1190        // relay-held op at that seq.
1191        assert!(matches!(
1192            relay.checkpoint_put("a", &forged),
1193            Err(RelayError::CheckpointFrontierUnverified { device_id, .. }) if device_id == "a"
1194        ));
1195
1196        // Nothing stored → GC has no coverage → a's real ops are NOT dropped.
1197        // (All probing calls below use "a" as the caller so no extra
1198        // never-acked device is auto-registered — which would pin the stable
1199        // frontier to None and mask the final GC assertion.)
1200        assert_eq!(relay.gc().unwrap().total(), 0, "no forged coverage, no data loss");
1201        assert!(relay.checkpoint_get().unwrap().is_none());
1202        // a's real ops are all still served.
1203        let served = relay.pull("a", &Frontier::new()).unwrap();
1204        assert_eq!(served.ops.len(), 3);
1205        assert!(served.ops.iter().all(|op| op.payload["id"].as_str().unwrap().starts_with("real-")));
1206
1207        // A checkpoint claiming coverage BEYOND the relay's chain head is
1208        // rejected too (not just a head mismatch).
1209        let mut a2 = DeviceLog::resume("a", &real).unwrap();
1210        let mut ahead = real.clone();
1211        ahead.push(a2.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": "real-3"})));
1212        let claims_beyond = Checkpoint::from_ops(&ahead).unwrap(); // relay never got real-3
1213        assert!(matches!(
1214            relay.checkpoint_put("a", &claims_beyond),
1215            Err(RelayError::CheckpointFrontierUnverified { seq: 3, .. })
1216        ));
1217
1218        // A checkpoint claiming a device the relay has never seen is rejected
1219        // (the frontier device "c" is validated regardless of the caller).
1220        let mut c = DeviceLog::new("c");
1221        let c_ops: Vec<OpRecord> = (0..2)
1222            .map(|i| c.append(Scope::Personal, Surface::Knowledge, serde_json::json!({"id": format!("c{i}")})))
1223            .collect();
1224        let unknown_dev = Checkpoint::from_ops(&c_ops).unwrap();
1225        assert!(matches!(
1226            relay.checkpoint_put("a", &unknown_dev),
1227            Err(RelayError::CheckpointFrontierUnverified { device_id, .. }) if device_id == "c"
1228        ));
1229
1230        // The HONEST checkpoint over a's real chain is accepted and DOES
1231        // become coverage — the fix rejects forgeries, not legitimacy.
1232        let honest = Checkpoint::from_ops(&real[..2]).unwrap();
1233        assert!(relay.checkpoint_put("a", &honest).unwrap());
1234        assert_eq!(relay.gc().unwrap().dropped["a"], 2);
1235    }
1236
1237    #[test]
1238    fn checkpoint_dedup_keys_on_whole_record_address_not_state_hash() {
1239        // The B4 contract, enforced at the relay: two checkpoints folding
1240        // to the SAME state under DIFFERENT frontiers are BOTH stored.
1241        // (Knowledge is content-keyed so two devices' identical facts dedup to
1242        // one folded state — a conversation turn would not, being an event
1243        // stream keyed by op_id.)
1244        let mut a = DeviceLog::new("a");
1245        let mut b = DeviceLog::new("b");
1246        let fact = serde_json::json!({"id": "f1", "body": "hi"});
1247        let oa = a.append(Scope::Personal, Surface::Knowledge, fact.clone());
1248        let ob = b.append(Scope::Personal, Surface::Knowledge, fact);
1249        let just_a = Checkpoint::from_ops(std::slice::from_ref(&oa)).unwrap();
1250        let both = Checkpoint::from_ops(&[oa.clone(), ob.clone()]).unwrap();
1251        assert_eq!(just_a.state_hash, both.state_hash, "the cross-device dedup collision");
1252
1253        let mut relay = mem_relay();
1254        // The relay must hold the real chains a checkpoint claims to cover —
1255        // checkpoint_put now cross-checks the frontier (data-loss fix).
1256        relay.push("a", std::slice::from_ref(&oa)).unwrap();
1257        relay.push("b", std::slice::from_ref(&ob)).unwrap();
1258        assert!(relay.checkpoint_put("a", &just_a).unwrap());
1259        assert!(relay.checkpoint_put("b", &both).unwrap(), "same state_hash is NOT a dedup");
1260        assert!(!relay.checkpoint_put("a", &just_a).unwrap(), "same checkpoint_hash IS");
1261
1262        // The latest pointer sits on the dominating frontier and a stale
1263        // re-put cannot regress it.
1264        assert_eq!(
1265            relay.checkpoint_get().unwrap().unwrap().checkpoint_hash,
1266            both.checkpoint_hash
1267        );
1268        relay.checkpoint_put("a", &just_a).unwrap();
1269        assert_eq!(
1270            relay.checkpoint_get().unwrap().unwrap().checkpoint_hash,
1271            both.checkpoint_hash,
1272            "dominance-monotone pointer"
1273        );
1274
1275        // A tampered checkpoint is refused.
1276        let mut forged = both.clone();
1277        forged.state.logs.clear();
1278        assert!(matches!(
1279            relay.checkpoint_put("b", &forged),
1280            Err(RelayError::Checkpoint(CheckpointError::HashMismatch { .. }))
1281        ));
1282    }
1283
1284    #[test]
1285    fn eviction_unpins_the_frontier_and_ack_reinstates() {
1286        let (t, wall) = manual_clock();
1287        let mut relay = InMemoryRelay::new(
1288            RelayConfig { eviction_horizon_ms: Some(1_000) },
1289            wall,
1290        );
1291        let (_, a_ops) = ops_for("a", 2);
1292        relay.push("a", &a_ops).unwrap();
1293
1294        t.store(100, Ordering::SeqCst);
1295        relay.ack("a", a_ops[1].hlc.clone()).unwrap();
1296        relay.ack("c", a_ops[0].hlc.clone()).unwrap(); // straggler acks early…
1297        assert_eq!(relay.stable_frontier().unwrap(), Some(a_ops[0].hlc.clone()));
1298
1299        // …then goes silent past H. Any other contact sweeps it out.
1300        t.store(2_000, Ordering::SeqCst);
1301        relay.register("a").unwrap();
1302        let roster: BTreeMap<String, RosterEntry> = relay
1303            .roster()
1304            .unwrap()
1305            .into_iter()
1306            .map(|e| (e.device_id.clone(), e))
1307            .collect();
1308        assert_eq!(roster["c"].status, DeviceStatus::Evicted);
1309        assert_eq!(roster["a"].status, DeviceStatus::Active);
1310        assert_eq!(
1311            relay.stable_frontier().unwrap(),
1312            Some(a_ops[1].hlc.clone()),
1313            "the evicted device's ack no longer holds the frontier"
1314        );
1315
1316        // A stale ack from the returning straggler does NOT reinstate (it
1317        // would drag GC eligibility back)…
1318        let outcome = relay.ack("c", a_ops[0].hlc.clone()).unwrap();
1319        assert!(!outcome.advanced && !outcome.reinstated);
1320        assert_eq!(relay.stable_frontier().unwrap(), Some(a_ops[1].hlc.clone()));
1321
1322        // …a caught-up ack does.
1323        let outcome = relay.ack("c", a_ops[1].hlc.clone()).unwrap();
1324        assert!(outcome.advanced && outcome.reinstated);
1325        let roster: BTreeMap<String, RosterEntry> = relay
1326            .roster()
1327            .unwrap()
1328            .into_iter()
1329            .map(|e| (e.device_id.clone(), e))
1330            .collect();
1331        assert_eq!(roster["c"].status, DeviceStatus::Active);
1332    }
1333
1334    #[test]
1335    fn fs_relay_matches_in_memory_semantics_and_persists() {
1336        let dir = tempfile::tempdir().unwrap();
1337        let (_, ops) = ops_for("a", 3);
1338        {
1339            let mut relay =
1340                FsRelay::open(dir.path(), RelayConfig::default(), Arc::new(|| 7)).unwrap();
1341            assert_eq!(relay.push("a", &ops).unwrap().accepted, 3);
1342            relay.ack("a", ops[2].hlc.clone()).unwrap();
1343            let ckpt = Checkpoint::from_ops(&ops[..2]).unwrap();
1344            assert!(relay.checkpoint_put("a", &ckpt).unwrap());
1345        }
1346        // A second handle (fresh process) sees the same durable state.
1347        let mut relay =
1348            FsRelay::open(dir.path(), RelayConfig::default(), Arc::new(|| 8)).unwrap();
1349        assert_eq!(relay.stable_frontier().unwrap(), Some(ops[2].hlc.clone()));
1350        // The pull auto-registers b — an active never-acked device drops
1351        // the stable frontier to None (nothing droppable) until b acks.
1352        assert_eq!(relay.pull("b", &Frontier::new()).unwrap().ops, ops);
1353        assert_eq!(relay.stable_frontier().unwrap(), None);
1354        relay.ack("b", ops[2].hlc.clone()).unwrap();
1355        assert_eq!(relay.push("a", &ops).unwrap(), PushOutcome { accepted: 0, deduped: 3 });
1356        let report = relay.gc().unwrap();
1357        assert_eq!(report.dropped["a"], 2);
1358        let roster = relay.roster().unwrap();
1359        assert_eq!(roster.len(), 2);
1360        assert_eq!(roster[0].added_at.wall_ms, 7, "roster added_at survives restart");
1361
1362        // The checkpoint round-trips through its content-addressed file
1363        // (Checkpoint::load re-verified it).
1364        let ckpt = relay.checkpoint_get().unwrap().unwrap();
1365        assert_eq!(ckpt.frontier["a"].seq, 1);
1366
1367        // A tampered on-disk checkpoint file is refused by the next process
1368        // to open the relay (a fresh handle with a cold cache re-verifies —
1369        // the two-Mac shared-folder threat model). The current handle keeps
1370        // serving the version it already verified into its cache (it never
1371        // serves unverified content), which is why the check uses a fresh
1372        // handle.
1373        let path = dir
1374            .path()
1375            .join("checkpoints")
1376            .join(format!("{}.checkpoint.json", ckpt.checkpoint_hash));
1377        let raw = fs::read_to_string(&path).unwrap();
1378        fs::write(&path, raw.replace("\"a-f0\"", "\"a-f0-forged\"")).unwrap();
1379        let mut fresh =
1380            FsRelay::open(dir.path(), RelayConfig::default(), Arc::new(|| 9)).unwrap();
1381        assert!(matches!(
1382            fresh.checkpoint_get(),
1383            Err(RelayError::Checkpoint(_))
1384        ));
1385    }
1386}