1use sha2::{Digest, Sha256};
17
18const EDITION_LABEL: &[u8] = b"vector-community/v1/edition";
20
21pub 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
53pub 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#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct Edition {
70 pub version: u64,
71 pub prev_hash: Option<[u8; 32]>,
72 pub self_hash: [u8; 32],
74 pub created_at: u64,
76 pub tiebreak_id: [u8; 32],
78}
79
80#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct FoldResult {
83 pub head: Option<usize>,
85 pub gap: bool,
88}
89
90pub fn fold(editions: &[Edition], floor: u64, floor_hash: Option<&[u8; 32]>) -> FoldResult {
106 use std::collections::BTreeMap;
107 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 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 let lo = &editions[by_version[&versions[0]]];
136 let anchored = if floor == 0 {
137 versions[0] == 1 && lo.prev_hash.is_none()
139 } else if versions[0] == floor {
140 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 };
150 let mut gap = !anchored;
151 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; break;
163 }
164 }
165 FoldResult { head: Some(head_idx), gap }
166}
167
168pub 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; }
182 match best {
183 Some(b) => {
184 let cur = &editions[b];
185 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 #[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 #[test]
219 fn edition_hash_is_field_unambiguous() {
220 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 #[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 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 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 assert_eq!(bootstrap_head(&grant, 9), None, "all below floor → no head");
262
263 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 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 #[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 #[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 #[test]
305 fn multiple_gaps_strict_stops_at_first_bootstrap_takes_highest() {
306 let eds = vec![linked(1), linked(2), linked(4), linked(6)]; 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 #[test]
316 fn ratchet_advances_when_the_missing_version_arrives() {
317 let before = vec![linked(1), linked(3)]; 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)]; 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 #[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 #[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); 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 #[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 #[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 #[test]
368 fn fold_version_zero_does_not_panic() {
369 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 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 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 let mut eds: Vec<Edition> = (1..=50).map(linked).collect();
411 eds.extend((1..=50).map(linked)); 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 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 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 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 #[test]
450 fn tracking_rejects_a_forked_floor_edition() {
451 let a_hash = id(0xAA);
452 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 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 #[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 let r = fold(&[e1, e2], 2, Some(&id(2)));
468 assert_eq!(r.head, Some(1));
469 assert!(!r.gap);
470 }
471
472 #[test]
475 fn equal_version_fork_resolves_by_lower_inner_id_not_created_at() {
476 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 #[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 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 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}