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