Skip to main content

vector_core/community/
version.rs

1//! Per-entity version chain for authority editions.
2//!
3//! Every authority record (a Grant, RoleMetadata, RoleOrder, Banlist, the OwnerAttestation) is a
4//! sequence of **editions**. Each edition carries a monotonic `version` and the hash of its
5//! predecessor (`prev_hash`), and the actor's real-npub signature covers both — so the chain is over
6//! signed content, not the (ephemeral) outer wrapper. Clients fold the fetched set into the current
7//! head by the rules:
8//!   - **refuse-downgrade** on the version integer (never accept a version below the floor already held);
9//!   - **equal-version fork** resolves by a deterministic tiebreak: the lower **inner edition id** (a
10//!     commitment hash over author+content+tags+time, NOT the author-settable `created_at`, so it can't
11//!     be cheaply biased — the authority-first lens is layered on by the caller via the roster);
12//!   - a **gap** (a higher version whose `prev_hash` doesn't link contiguously to what we hold) leaves
13//!     the head at the highest *contiguous* version and is reported, so the caller can fail closed for
14//! that entity and refetch the missing prereqs from the quorum (H1/M8) rather than fail open.
15
16use sha2::{Digest, Sha256};
17
18/// Frozen domain-separation label for the edition canonicalization (never change).
19const EDITION_LABEL: &[u8] = b"vector-community/v1/edition";
20
21/// Domain-separated, length-prefixed canonical bytes an authority edition commits to.
22///
23/// Layout (FROZEN — interop + no-migration depend on it):
24/// `u64_be(label.len) ‖ label ‖ entity_id[32] ‖ u64_be(version) ‖ has_prev(1) ‖ prev_hash[32 or zero]
25///  ‖ u64_be(content.len) ‖ content`.
26/// Every field is fixed-width or length-prefixed so distinct inputs can never collide.
27pub fn edition_signing_bytes(
28    entity_id: &[u8; 32],
29    version: u64,
30    prev_hash: Option<&[u8; 32]>,
31    content: &[u8],
32) -> Vec<u8> {
33    let mut out = Vec::with_capacity(8 + EDITION_LABEL.len() + 32 + 8 + 1 + 32 + 8 + content.len());
34    out.extend_from_slice(&(EDITION_LABEL.len() as u64).to_be_bytes());
35    out.extend_from_slice(EDITION_LABEL);
36    out.extend_from_slice(entity_id);
37    out.extend_from_slice(&version.to_be_bytes());
38    match prev_hash {
39        Some(h) => {
40            out.push(1);
41            out.extend_from_slice(h);
42        }
43        None => {
44            out.push(0);
45            out.extend_from_slice(&[0u8; 32]);
46        }
47    }
48    out.extend_from_slice(&(content.len() as u64).to_be_bytes());
49    out.extend_from_slice(content);
50    out
51}
52
53/// SHA-256 of [`edition_signing_bytes`] — the edition's identity in the chain. The next edition's
54/// `prev_hash` cites this value.
55pub fn edition_hash(
56    entity_id: &[u8; 32],
57    version: u64,
58    prev_hash: Option<&[u8; 32]>,
59    content: &[u8],
60) -> [u8; 32] {
61    let mut h = Sha256::new();
62    h.update(edition_signing_bytes(entity_id, version, prev_hash, content));
63    h.finalize().into()
64}
65
66/// One fetched edition of an entity, reduced to what the fold needs. (Signature/authority validation
67/// happens before this — only editions whose real-npub signature verified are folded.)
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Edition {
70    pub version: u64,
71    pub prev_hash: Option<[u8; 32]>,
72    /// `edition_hash` of THIS edition (what the next edition's `prev_hash` must cite).
73    pub self_hash: [u8; 32],
74    /// Inner authored timestamp (secs); the first tiebreak at equal version.
75    pub created_at: u64,
76    /// Inner event id; the deterministic final tiebreak (same for every member).
77    pub tiebreak_id: [u8; 32],
78}
79
80/// The outcome of folding one entity's editions.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct FoldResult {
83    /// Index (into the input slice) of the chosen head edition, or `None` if nothing ≥ floor.
84    pub head: Option<usize>,
85    /// A higher version exists but doesn't link contiguously to the head — withheld prereqs. The
86    /// caller fails CLOSED for the entity (suspends its authority) and refetches (H1/M8).
87    pub gap: bool,
88    /// The BASE anchor test passed: the lowest ≥-floor edition is a genesis, our exact held floor
89    /// edition, or chains directly to it. When true, `head` is a chain-verified extension of the
90    /// committed floor even if `gap` flags withheld versions ABOVE it — adopting that verified
91    /// prefix is sound (refuse-downgrade holds). When false, `head` is unanchored: do not adopt.
92    pub anchored: bool,
93}
94
95/// Fold a set of editions for **one** entity into its current head.
96///
97/// `floor` is the highest version the client has already accepted (0 = none yet) and `floor_hash` is
98/// that held edition's [`Edition::self_hash`] (so a new edition can be proven to link to it).
99/// Editions below the floor are ignored (refuse-downgrade), equal-version forks pick the
100/// deterministic tiebreak winner, and the head walks the contiguous `prev_hash` chain upward.
101///
102/// **`gap` is the safety signal.** It is set whenever the head is NOT chain-anchored — either the
103/// lowest edition isn't a genesis / doesn't link to `floor_hash`, or a link breaks mid-chain. A
104/// **tracking** client (one that already holds the floor) MUST fail closed on `gap` (suspend the
105/// entity and refetch the missing prereqs from the quorum — H1/M8), since an unanchored head can
106/// be a forged or rolled-back edition (a hostile relay serving only a high version). A
107/// **bootstrapping** client (a new joiner, `floor == 0`, who legitimately lacks history because the
108/// state was re-anchored under a later epoch) may accept the head despite `gap` *only* after
109/// independently verifying its author's current authority against the roster + owner attestation.
110pub fn fold(editions: &[Edition], floor: u64, floor_hash: Option<&[u8; 32]>) -> FoldResult {
111    use std::collections::BTreeMap;
112    // Per-version winner (equal-version fork → lower tiebreak_id). Skip anything below the floor:
113    // refuse-downgrade.
114    let mut by_version: BTreeMap<u64, usize> = BTreeMap::new();
115    for (i, e) in editions.iter().enumerate() {
116        if e.version < floor {
117            continue;
118        }
119        match by_version.get(&e.version) {
120            Some(&j) => {
121                let cur = &editions[j];
122                // Equal-version fork → lower inner edition id wins. The id is a commitment hash over
123                // (author, content, tags, time), NOT the author-settable `created_at`, so the winner is
124                // deterministic for every client and can't be cheaply gamed (no `created_at=0` always-win).
125                if e.tiebreak_id < cur.tiebreak_id {
126                    by_version.insert(e.version, i);
127                }
128            }
129            None => {
130                by_version.insert(e.version, i);
131            }
132        }
133    }
134    let versions: Vec<u64> = by_version.keys().copied().collect();
135    if versions.is_empty() {
136        return FoldResult { head: None, gap: false, anchored: false };
137    }
138    // Anchor the lowest edition — the chain must be rooted, not merely internally linked. Without
139    // this a lone high-version edition with a forged prev_hash would be trusted as a contiguous head.
140    let lo = &editions[by_version[&versions[0]]];
141    let anchored = if floor == 0 {
142        // No prior head held → only a genuine genesis (v1, no predecessor) anchors the chain.
143        versions[0] == 1 && lo.prev_hash.is_none()
144    } else if versions[0] == floor {
145        // Re-presenting the held edition (e.g. re-anchored under a new epoch — which re-seals the SAME
146        // inner edition, so its self_hash is identical). It MUST be the exact edition we committed to:
147        // otherwise a relay that withholds ours and serves a DIFFERENT, same-version fork would silently
148        // replace our floor. The hash check rejects the fork → gap → fail closed → refetch.
149        floor_hash == Some(&lo.self_hash)
150    } else if versions[0] == floor + 1 {
151        floor_hash.is_some() && lo.prev_hash.as_ref() == floor_hash
152    } else {
153        false // a jump past the floor with the linking edition(s) missing
154    };
155    let mut gap = !anchored;
156    // Walk upward; advance only across a contiguous link (version == prev+1 AND prev_hash matches).
157    let mut head_idx = by_version[&versions[0]];
158    for pair in versions.windows(2) {
159        let lo_idx = by_version[&pair[0]];
160        let hi_idx = by_version[&pair[1]];
161        let linked = pair[1] == pair[0] + 1
162            && editions[hi_idx].prev_hash == Some(editions[lo_idx].self_hash);
163        if linked {
164            head_idx = hi_idx;
165        } else {
166            gap = true; // a higher version exists but isn't contiguously linked
167            break;
168        }
169    }
170    FoldResult { head: Some(head_idx), gap, anchored }
171}
172
173/// The head a **bootstrapping** client accepts: the per-version winner at the HIGHEST present
174/// version ≥ `floor`, **ignoring chain contiguity**. A fresh joiner whose genesis was re-anchored away
175/// cannot verify lineage at all, so contiguity is the wrong test for it — the real gate is the
176/// edition's signature (verified before folding) plus the author's CURRENT authority, which the caller
177/// resolves against the roster + owner attestation. A relay cannot forge a higher version (no valid
178/// signature), so the worst case is a stale-but-valid head that the union + ratchet later upgrade.
179/// Returns `None` if no edition is ≥ `floor`. Equal-version forks use the same deterministic tiebreak
180/// as [`fold`] (lower inner edition id) so every client converges on one head.
181pub fn bootstrap_head(editions: &[Edition], floor: u64) -> Option<usize> {
182    let mut best: Option<usize> = None;
183    for (i, e) in editions.iter().enumerate() {
184        if e.version < floor {
185            continue; // refuse-downgrade still applies
186        }
187        match best {
188            Some(b) => {
189                let cur = &editions[b];
190                // Higher version wins; at equal version, the lower inner edition id (see `fold`).
191                let take = e.version > cur.version
192                    || (e.version == cur.version && e.tiebreak_id < cur.tiebreak_id);
193                if take {
194                    best = Some(i);
195                }
196            }
197            None => best = Some(i),
198        }
199    }
200    best
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn id(b: u8) -> [u8; 32] {
208        [b; 32]
209    }
210
211    /// Golden vector — the frozen edition canonicalization must never drift (a change reshuffles
212    /// every chain link and forces a migration).
213    #[test]
214    fn edition_hash_golden_vector() {
215        let h = edition_hash(&id(0x11), 1, None, b"hello");
216        assert_eq!(
217            crate::simd::hex::bytes_to_hex_32(&h),
218            "2daf42e65a6bc259a4c99fac6df754a5d3d92310607cf13e2a1e8c94d42f6303"
219        );
220    }
221
222    /// Distinct fields never collide (length-prefixing): same bytes split differently differ.
223    #[test]
224    fn edition_hash_is_field_unambiguous() {
225        // version vs content boundary can't be confused.
226        assert_ne!(
227            edition_hash(&id(1), 2, None, b"x"),
228            edition_hash(&id(1), 0, None, b"x"),
229        );
230        let with_prev = edition_hash(&id(1), 2, Some(&id(9)), b"x");
231        let without = edition_hash(&id(1), 2, None, b"x");
232        assert_ne!(with_prev, without, "prev presence changes the hash");
233    }
234
235    /// A linked v1→v2→v3 chain folds to v3 with no gap.
236    #[test]
237    fn contiguous_chain_folds_to_latest() {
238        let e1 = Edition { version: 1, prev_hash: None, self_hash: id(1), created_at: 100, tiebreak_id: id(0xa1) };
239        let e2 = Edition { version: 2, prev_hash: Some(id(1)), self_hash: id(2), created_at: 101, tiebreak_id: id(0xa2) };
240        let e3 = Edition { version: 3, prev_hash: Some(id(2)), self_hash: id(3), created_at: 102, tiebreak_id: id(0xa3) };
241        let r = fold(&[e1, e2, e3], 0, None);
242        assert_eq!(r, FoldResult { head: Some(2), gap: false, anchored: true });
243    }
244
245    #[test]
246    fn bootstrap_head_takes_highest_version_across_gaps() {
247        let ed = |v: u64| Edition {
248            version: v,
249            prev_hash: if v == 1 { None } else { Some(id(v as u8 - 1)) },
250            self_hash: id(v as u8),
251            created_at: 100 + v,
252            tiebreak_id: id(0xa0 + v as u8),
253        };
254        // GroupRoot shape: 1,2,3,4,(no 5),6..11 — strict fold stops at v4; bootstrap takes v11.
255        let groot: Vec<Edition> = [1u64, 2, 3, 4, 6, 7, 8, 9, 10, 11].iter().map(|&v| ed(v)).collect();
256        assert_eq!(fold(&groot, 0, None).head.map(|i| groot[i].version), Some(4), "strict head stops at the gap");
257        assert!(fold(&groot, 0, None).gap);
258        assert_eq!(bootstrap_head(&groot, 0).map(|i| groot[i].version), Some(11), "bootstrap takes the latest across the gap");
259
260        // Grant shape: 2,3,4 with no v1 to anchor — strict can't anchor; bootstrap takes v4.
261        let grant: Vec<Edition> = [2u64, 3, 4].iter().map(|&v| ed(v)).collect();
262        assert!(fold(&grant, 0, None).gap, "no v1 → strict is unanchored");
263        assert_eq!(bootstrap_head(&grant, 0).map(|i| grant[i].version), Some(4));
264
265        // Refuse-downgrade still holds: nothing below the floor.
266        assert_eq!(bootstrap_head(&grant, 9), None, "all below floor → no head");
267
268        // Equal-version fork resolves to the deterministic tiebreak winner: lower inner id (a: 0xa1 < b: 0xb1).
269        let a = Edition { version: 5, prev_hash: None, self_hash: id(0xAA), created_at: 200, tiebreak_id: id(0xa1) };
270        let b = Edition { version: 5, prev_hash: None, self_hash: id(0xBB), created_at: 100, tiebreak_id: id(0xb1) };
271        assert_eq!(bootstrap_head(&[a, b], 0), Some(0), "lower inner id wins at equal version (not created_at)");
272    }
273
274    // A properly-linked edition: self_hash=id(v), prev=id(v-1) (genesis v1 has no prev).
275    fn linked(v: u64) -> Edition {
276        Edition {
277            version: v,
278            prev_hash: if v == 1 { None } else { Some(id((v - 1) as u8)) },
279            self_hash: id(v as u8),
280            created_at: 100 + v,
281            tiebreak_id: id(0xc0u8.wrapping_add(v as u8)),
282        }
283    }
284
285    /// AGGREGATE LINEARITY — no single relay has the whole chain (relay A: v1,3,5; relay B: v2,4), but the
286    /// UNION computes the full contiguous chain → head v5, no gap. This is the property the whole
287    /// "bad-relay resilient" design rests on: gaps in any one source are filled by the others. `fold_roster`
288    /// inherits this since it folds the per-entity union.
289    #[test]
290    fn union_of_split_relays_folds_contiguously() {
291        let mut union = vec![linked(1), linked(3), linked(5)];
292        union.extend(vec![linked(2), linked(4)]);
293        let r = fold(&union, 0, None);
294        assert_eq!(r.head.map(|i| union[i].version), Some(5));
295        assert!(!r.gap, "the union is contiguous v1..v5 even though neither relay had it alone");
296    }
297
298    /// Arrival order is irrelevant — the fold is a pure function of the SET (so two clients merging the same
299    /// editions in different orders converge identically).
300    #[test]
301    fn fold_is_order_independent_under_scrambled_arrival() {
302        let scrambled = vec![linked(3), linked(1), linked(5), linked(2), linked(4)];
303        let r = fold(&scrambled, 0, None);
304        assert_eq!(r.head.map(|i| scrambled[i].version), Some(5));
305        assert!(!r.gap);
306    }
307
308    /// Multiple holes: strict stops at the FIRST gap (fail-closed prefix), bootstrap takes the highest.
309    #[test]
310    fn multiple_gaps_strict_stops_at_first_bootstrap_takes_highest() {
311        let eds = vec![linked(1), linked(2), linked(4), linked(6)]; // holes at v3 and v5
312        let r = fold(&eds, 0, None);
313        assert_eq!(r.head.map(|i| eds[i].version), Some(2), "strict stops at the first gap");
314        assert!(r.gap);
315        assert_eq!(bootstrap_head(&eds, 0).map(|i| eds[i].version), Some(6), "bootstrap takes the highest");
316    }
317
318    /// The RATCHET click: a gap leaves the head behind; when the missing version streams in from the union,
319    /// the chain advances. Convergence is monotonic and order-free.
320    #[test]
321    fn ratchet_advances_when_the_missing_version_arrives() {
322        let before = vec![linked(1), linked(3)]; // v2 missing
323        let r1 = fold(&before, 0, None);
324        assert_eq!(r1.head.map(|i| before[i].version), Some(1));
325        assert!(r1.gap, "v3 can't link without v2");
326        let after = vec![linked(1), linked(2), linked(3)]; // v2 arrives
327        let r2 = fold(&after, 0, None);
328        assert_eq!(r2.head.map(|i| after[i].version), Some(3));
329        assert!(!r2.gap, "the gap filled → ratchets to v3");
330    }
331
332    /// Duplicate editions (a relay echo, or the same edition from two relays) don't double-count or break
333    /// the chain — dedup by version, fold proceeds cleanly.
334    #[test]
335    fn duplicate_editions_do_not_break_the_fold() {
336        let eds = vec![linked(1), linked(2), linked(2), linked(3)];
337        let r = fold(&eds, 0, None);
338        assert_eq!(r.head.map(|i| eds[i].version), Some(3));
339        assert!(!r.gap);
340    }
341
342    /// A forged MIDDLE edition (wrong prev) must not let a later "linked" edition advance the head —
343    /// the walk breaks at the bad link and the head stays at the last contiguous version.
344    #[test]
345    fn forged_middle_edition_does_not_advance_the_head() {
346        let e1 = linked(1);
347        let e2_bad = Edition { version: 2, prev_hash: Some(id(0xFF)), self_hash: id(2), created_at: 102, tiebreak_id: id(0xc2) };
348        let e3 = linked(3); // links to id(2) — but v2's link to v1 is forged
349        let r = fold(&[e1, e2_bad, e3], 0, None);
350        assert_eq!(r.head.map(|i| [1u64, 2, 3][i]), Some(1), "head stays at v1 — the v1→v2 link is broken");
351        assert!(r.gap, "a forged middle edition is a gap, not a silent advance");
352    }
353
354    /// A held floor whose hash we've lost (`floor_hash = None`) must FAIL CLOSED at floor+1, not blindly
355    /// anchor — without the hash we can't prove the incoming edition links to what we hold.
356    #[test]
357    fn floor_plus_one_without_a_floor_hash_is_a_gap() {
358        let e6 = Edition { version: 6, prev_hash: Some(id(5)), self_hash: id(6), created_at: 600, tiebreak_id: id(0xa6) };
359        assert!(fold(&[e6], 5, None).gap, "floor+1 with no floor_hash can't be anchored → gap (fail closed)");
360    }
361
362    /// A relay serving ONLY stale editions (all below floor) yields "no change" — `head: None, gap: false`
363    /// — distinct from a gap. The caller keeps its floor; it neither quarantines nor re-authorizes.
364    #[test]
365    fn all_below_floor_is_no_change_not_a_gap() {
366        let r = fold(&[linked(1), linked(2)], 5, Some(&id(5)));
367        assert_eq!(r, FoldResult { head: None, gap: false, anchored: false }, "everything below floor → no candidate, no gap");
368    }
369
370    // ===== Weird / absurd / malformed input — fold must degrade gracefully, NEVER panic =====
371
372    #[test]
373    fn fold_version_zero_does_not_panic() {
374        // v0 is invalid (chains start at v1) but a relay could serve one. Unanchored → gap, no panic.
375        let e0 = Edition { version: 0, prev_hash: None, self_hash: id(0), created_at: 1, tiebreak_id: id(0xe0) };
376        assert!(fold(&[e0], 0, None).gap, "a v0 'genesis' is not a valid anchor → gap, no panic");
377    }
378
379    #[test]
380    fn fold_genesis_with_a_spurious_prev_is_unanchored() {
381        // A v1 carrying a prev_hash (a real genesis has none) is a forged "genesis" → unanchored.
382        let e1 = Edition { version: 1, prev_hash: Some(id(0xFF)), self_hash: id(1), created_at: 100, tiebreak_id: id(0xa1) };
383        assert!(fold(&[e1], 0, None).gap, "v1 with a prev is not a real genesis → gap");
384    }
385
386    #[test]
387    fn fold_near_u64_max_version_does_not_panic() {
388        // A relay serves a wildly high version. fold uses it as a key; the +1 increment lives in the
389        // producer, not here, so no overflow. Must not panic from any floor.
390        let big = Edition { version: u64::MAX, prev_hash: None, self_hash: id(9), created_at: 1, tiebreak_id: id(0xff) };
391        assert!(fold(&[big.clone()], 0, None).gap, "u64::MAX is not a genesis → gap, no overflow");
392        let r = fold(&[big], u64::MAX, Some(&id(9)));
393        assert!(!r.gap, "re-presenting the held u64::MAX floor is anchored, no overflow in the walk");
394    }
395
396    #[test]
397    fn fold_a_million_version_gap_holds_at_the_prefix() {
398        let far = Edition { version: 1_000_000, prev_hash: Some(id(2)), self_hash: id(0x77), created_at: 200, tiebreak_id: id(0xb7) };
399        let r = fold(&[linked(1), far], 0, None);
400        assert_eq!(r.head.map(|i| [1u64, 1_000_000][i]), Some(1), "head stays at the genesis prefix");
401        assert!(r.gap, "a million-version jump is a gap, not a silent advance");
402    }
403
404    #[test]
405    fn fold_mass_identical_duplicates_collapse() {
406        let e = linked(1);
407        let r = fold(&[e.clone(), e.clone(), e.clone(), e], 0, None);
408        assert_eq!(r.head, Some(0));
409        assert!(!r.gap, "N identical genesis copies collapse to one, no gap");
410    }
411
412    #[test]
413    fn fold_a_large_noisy_scrambled_input_does_not_panic() {
414        // A contiguous v1..v50 chain buried in duplicates and reversed arrival order.
415        let mut eds: Vec<Edition> = (1..=50).map(linked).collect();
416        eds.extend((1..=50).map(linked)); // every edition twice
417        eds.reverse();
418        let r = fold(&eds, 0, None);
419        assert_eq!(r.head.map(|i| eds[i].version), Some(50), "folds to v50 through all the noise");
420        assert!(!r.gap);
421    }
422
423    #[test]
424    fn bootstrap_head_on_pathological_inputs() {
425        assert_eq!(bootstrap_head(&[], 0), None, "empty → None");
426        assert_eq!(bootstrap_head(&[linked(1), linked(2)], 999), None, "all below floor → None");
427        let v0 = Edition { version: 0, prev_hash: None, self_hash: id(0), created_at: 1, tiebreak_id: id(0xe0) };
428        assert!(bootstrap_head(&[v0], 0).is_some(), "a v0 edition still surfaces (≥ floor 0), no panic");
429    }
430
431    #[test]
432    fn unanchored_head_is_flagged_as_gap() {
433        // A hostile relay serves only v5 with a forged prev_hash, withholding v1..v4. The head is
434        // returned but MUST be flagged gap=true — it isn't anchored to genesis, so a tracking client
435        // fails closed instead of installing a possibly forged/rolled-back edition.
436        let e5 = Edition { version: 5, prev_hash: Some(id(0xFF)), self_hash: id(5), created_at: 500, tiebreak_id: id(0xa5) };
437        assert!(fold(&[e5], 0, None).gap, "a lone non-genesis edition is unanchored → gap");
438
439        // With the held floor + its hash, a genuine v6 linking to v5 IS anchored (no gap).
440        let floor_hash = id(0x55);
441        let e6 = Edition { version: 6, prev_hash: Some(floor_hash), self_hash: id(6), created_at: 600, tiebreak_id: id(0xa6) };
442        let r = fold(&[e6], 5, Some(&floor_hash));
443        assert_eq!(r, FoldResult { head: Some(0), gap: false, anchored: true }, "v6 linking to the held v5 hash is anchored");
444
445        // A v6 whose prev_hash does NOT match the held floor is unanchored → gap.
446        let e6_bad = Edition { version: 6, prev_hash: Some(id(0xAB)), self_hash: id(6), created_at: 600, tiebreak_id: id(0xa6) };
447        assert!(fold(&[e6_bad], 5, Some(&floor_hash)).gap, "v6 not linking to the floor is a gap");
448    }
449
450    /// At the FLOOR version, a re-presented edition must be the exact one we hold.
451    /// A relay that withholds our floor edition A and serves a DIFFERENT same-version fork B must be
452    /// rejected (gap → fail closed) so it can't silently swap our committed head. The genuine re-anchor
453    /// case (same inner edition → identical self_hash) still anchors cleanly.
454    #[test]
455    fn tracking_rejects_a_forked_floor_edition() {
456        let a_hash = id(0xAA);
457        // Re-presenting OUR floor edition (same self_hash) → anchored, no gap (the legit re-anchor path).
458        let a = Edition { version: 5, prev_hash: Some(id(4)), self_hash: a_hash, created_at: 500, tiebreak_id: id(0xa5) };
459        assert!(!fold(&[a], 5, Some(&a_hash)).gap, "re-presenting our own floor edition is anchored");
460        // A DIFFERENT edition at the floor version (a withheld-original fork) → gap, fail closed.
461        let b = Edition { version: 5, prev_hash: Some(id(4)), self_hash: id(0xBB), created_at: 600, tiebreak_id: id(0xb5) };
462        assert!(fold(&[b], 5, Some(&a_hash)).gap, "a different same-version edition is a fork → rejected, not anchored");
463    }
464
465    /// Refuse-downgrade: an edition below the floor is ignored entirely.
466    #[test]
467    fn refuses_to_downgrade_below_floor() {
468        let e1 = Edition { version: 1, prev_hash: None, self_hash: id(1), created_at: 100, tiebreak_id: id(0xa1) };
469        let e2 = Edition { version: 2, prev_hash: Some(id(1)), self_hash: id(2), created_at: 101, tiebreak_id: id(0xa2) };
470        // Floor already at 2 (we hold v2 = self_hash id(2)) → only v2 is a candidate; v1 is a downgrade and
471        // dropped. Pass the held floor hash, as production does — the ==floor anchor now verifies it.
472        let r = fold(&[e1, e2], 2, Some(&id(2)));
473        assert_eq!(r.head, Some(1));
474        assert!(!r.gap);
475    }
476
477    /// Equal-version fork resolves to the deterministic tiebreak winner (lower created_at, then id),
478    /// not to arrival order — so every client converges on the same head.
479    #[test]
480    fn equal_version_fork_resolves_by_lower_inner_id_not_created_at() {
481        // Two distinct v1 editions. Winner = lower inner edition id; `created_at` is IGNORED, so an author
482        // can't set created_at=0 to force a win. `a` has the LATER created_at but the LOWER id → `a` wins,
483        // proving created_at is not the lever (the anti-gaming fix).
484        let a = Edition { version: 1, prev_hash: None, self_hash: id(0xAA), created_at: 999, tiebreak_id: id(0x01) };
485        let b = Edition { version: 1, prev_hash: None, self_hash: id(0xBB), created_at: 0, tiebreak_id: id(0x02) };
486        assert_eq!(fold(&[a.clone(), b.clone()], 0, None).head, Some(0), "lower id wins even though `a` has the later created_at");
487        assert_eq!(fold(&[b, a], 0, None).head, Some(1), "and it's independent of arrival order");
488    }
489
490    /// A missing or mismatched predecessor is a gap: the head stays at the highest CONTIGUOUS version
491    /// and `gap` flags the break (caller fails closed + refetches).
492    #[test]
493    fn detects_a_gap_in_the_chain() {
494        let e1 = Edition { version: 1, prev_hash: None, self_hash: id(1), created_at: 100, tiebreak_id: id(0xa1) };
495        // v3 present but v2 missing → not contiguous from v1.
496        let e3 = Edition { version: 3, prev_hash: Some(id(2)), self_hash: id(3), created_at: 102, tiebreak_id: id(0xa3) };
497        let r = fold(&[e1.clone(), e3], 0, None);
498        assert_eq!(r.head, Some(0), "head stays at the highest contiguous version (v1)");
499        assert!(r.gap, "the v2 gap is reported");
500
501        // Present-but-wrong prev_hash is also a gap (a forked/forged link).
502        let e2_bad = Edition { version: 2, prev_hash: Some(id(0xFF)), self_hash: id(2), created_at: 101, tiebreak_id: id(0xa2) };
503        let r2 = fold(&[e1.clone(), e2_bad], 0, None);
504        assert_eq!(r2.head, Some(0));
505        assert!(r2.gap, "a wrong prev_hash link does not advance the head");
506    }
507}