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 pub anchored: bool,
93}
94
95pub fn fold(editions: &[Edition], floor: u64, floor_hash: Option<&[u8; 32]>) -> FoldResult {
111 use std::collections::BTreeMap;
112 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 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 let lo = &editions[by_version[&versions[0]]];
141 let anchored = if floor == 0 {
142 versions[0] == 1 && lo.prev_hash.is_none()
144 } else if versions[0] == floor {
145 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 };
155 let mut gap = !anchored;
156 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; break;
168 }
169 }
170 FoldResult { head: Some(head_idx), gap, anchored }
171}
172
173pub 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; }
187 match best {
188 Some(b) => {
189 let cur = &editions[b];
190 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 #[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 #[test]
224 fn edition_hash_is_field_unambiguous() {
225 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 #[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 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 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 assert_eq!(bootstrap_head(&grant, 9), None, "all below floor → no head");
267
268 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 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 #[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 #[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 #[test]
310 fn multiple_gaps_strict_stops_at_first_bootstrap_takes_highest() {
311 let eds = vec![linked(1), linked(2), linked(4), linked(6)]; 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 #[test]
321 fn ratchet_advances_when_the_missing_version_arrives() {
322 let before = vec![linked(1), linked(3)]; 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)]; 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 #[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 #[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); 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 #[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 #[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 #[test]
373 fn fold_version_zero_does_not_panic() {
374 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 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 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 let mut eds: Vec<Edition> = (1..=50).map(linked).collect();
416 eds.extend((1..=50).map(linked)); 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 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 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 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 #[test]
455 fn tracking_rejects_a_forked_floor_edition() {
456 let a_hash = id(0xAA);
457 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 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 #[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 let r = fold(&[e1, e2], 2, Some(&id(2)));
473 assert_eq!(r.head, Some(1));
474 assert!(!r.gap);
475 }
476
477 #[test]
480 fn equal_version_fork_resolves_by_lower_inner_id_not_created_at() {
481 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 #[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 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 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}