1pub const MARKER_SEGMENT_MAGIC: [u8; 4] = *b"FSMK";
15
16pub const MARKER_FORMAT_VERSION: u32 = 1;
18
19pub const MARKER_SEGMENT_HEADER_BYTES: usize = 36;
21
22pub const COMMIT_MARKER_RECORD_BYTES: usize = 88;
24
25pub const MARKERS_PER_SEGMENT: u64 = 1_000_000;
27
28const MARKER_ID_DOMAIN: &[u8] = b"fsqlite:marker:v1";
30
31const ID_SIZE: usize = 16;
33
34const RECORD_PREFIX_BYTES: usize = 64;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum MarkerError {
46 HeaderTooShort,
48 BadMagic,
50 HeaderChecksumMismatch { expected: u64, actual: u64 },
52 RecordTooShort,
54 RecordChecksumMismatch { expected: u64, actual: u64 },
56 UnsupportedVersion { version: u32 },
58 RecordSizeMismatch { expected: u32, actual: u32 },
60 CommitSeqMismatch { expected: u64, actual: u64 },
62 TornTail {
64 complete_records: u64,
65 trailing_bytes: usize,
66 },
67}
68
69impl std::fmt::Display for MarkerError {
70 #[allow(clippy::too_many_lines)]
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::HeaderTooShort => f.write_str("marker segment header too short"),
74 Self::BadMagic => f.write_str("bad magic in marker segment header"),
75 Self::HeaderChecksumMismatch { expected, actual } => {
76 write!(
77 f,
78 "marker header xxh3 mismatch: expected {expected:#018X}, got {actual:#018X}"
79 )
80 }
81 Self::RecordTooShort => f.write_str("commit marker record too short"),
82 Self::RecordChecksumMismatch { expected, actual } => {
83 write!(
84 f,
85 "marker record xxh3 mismatch: expected {expected:#018X}, got {actual:#018X}"
86 )
87 }
88 Self::UnsupportedVersion { version } => {
89 write!(f, "unsupported marker format version: {version}")
90 }
91 Self::RecordSizeMismatch { expected, actual } => {
92 write!(
93 f,
94 "marker record size mismatch: expected {expected}, got {actual}"
95 )
96 }
97 Self::CommitSeqMismatch { expected, actual } => {
98 write!(f, "commit_seq mismatch: expected {expected}, got {actual}")
99 }
100 Self::TornTail {
101 complete_records,
102 trailing_bytes,
103 } => {
104 write!(
105 f,
106 "torn tail: {complete_records} complete records, {trailing_bytes} trailing bytes"
107 )
108 }
109 }
110 }
111}
112
113impl std::error::Error for MarkerError {}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct MarkerSegmentHeader {
132 pub segment_id: u64,
134 pub start_commit_seq: u64,
136}
137
138impl MarkerSegmentHeader {
139 #[must_use]
141 pub const fn new(segment_id: u64, start_commit_seq: u64) -> Self {
142 Self {
143 segment_id,
144 start_commit_seq,
145 }
146 }
147
148 #[must_use]
150 pub fn encode(&self) -> [u8; MARKER_SEGMENT_HEADER_BYTES] {
151 let mut buf = [0u8; MARKER_SEGMENT_HEADER_BYTES];
152 buf[0..4].copy_from_slice(&MARKER_SEGMENT_MAGIC);
154 buf[4..8].copy_from_slice(&MARKER_FORMAT_VERSION.to_le_bytes());
156 buf[8..16].copy_from_slice(&self.segment_id.to_le_bytes());
158 buf[16..24].copy_from_slice(&self.start_commit_seq.to_le_bytes());
160 #[allow(clippy::cast_possible_truncation)]
162 let record_size = COMMIT_MARKER_RECORD_BYTES as u32;
163 buf[24..28].copy_from_slice(&record_size.to_le_bytes());
164 let hash = xxhash_rust::xxh3::xxh3_64(&buf[..28]);
166 buf[28..36].copy_from_slice(&hash.to_le_bytes());
167 buf
168 }
169
170 pub fn decode(data: &[u8]) -> Result<Self, MarkerError> {
172 if data.len() < MARKER_SEGMENT_HEADER_BYTES {
173 return Err(MarkerError::HeaderTooShort);
174 }
175
176 if data[0..4] != MARKER_SEGMENT_MAGIC {
178 return Err(MarkerError::BadMagic);
179 }
180
181 let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
183 if version != MARKER_FORMAT_VERSION {
184 return Err(MarkerError::UnsupportedVersion { version });
185 }
186
187 let segment_id = u64::from_le_bytes(data[8..16].try_into().expect("8 bytes"));
189
190 let start_commit_seq = u64::from_le_bytes(data[16..24].try_into().expect("8 bytes"));
192
193 let record_size = u32::from_le_bytes([data[24], data[25], data[26], data[27]]);
195 #[allow(clippy::cast_possible_truncation)]
196 let expected_record_size = COMMIT_MARKER_RECORD_BYTES as u32;
197 if record_size != expected_record_size {
198 return Err(MarkerError::RecordSizeMismatch {
199 expected: expected_record_size,
200 actual: record_size,
201 });
202 }
203
204 let stored_hash = u64::from_le_bytes(data[28..36].try_into().expect("8 bytes"));
206 let computed_hash = xxhash_rust::xxh3::xxh3_64(&data[..28]);
207 if stored_hash != computed_hash {
208 return Err(MarkerError::HeaderChecksumMismatch {
209 expected: computed_hash,
210 actual: stored_hash,
211 });
212 }
213
214 Ok(Self {
215 segment_id,
216 start_commit_seq,
217 })
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct CommitMarkerRecord {
239 pub commit_seq: u64,
241 pub commit_time_unix_ns: u64,
243 pub capsule_object_id: [u8; ID_SIZE],
245 pub proof_object_id: [u8; ID_SIZE],
247 pub prev_marker_id: [u8; ID_SIZE],
249 pub marker_id: [u8; ID_SIZE],
251}
252
253impl CommitMarkerRecord {
254 #[must_use]
256 pub fn new(
257 commit_seq: u64,
258 commit_time_unix_ns: u64,
259 capsule_object_id: [u8; ID_SIZE],
260 proof_object_id: [u8; ID_SIZE],
261 prev_marker_id: [u8; ID_SIZE],
262 ) -> Self {
263 let marker_id = compute_marker_id(
264 commit_seq,
265 commit_time_unix_ns,
266 &capsule_object_id,
267 &proof_object_id,
268 &prev_marker_id,
269 );
270 Self {
271 commit_seq,
272 commit_time_unix_ns,
273 capsule_object_id,
274 proof_object_id,
275 prev_marker_id,
276 marker_id,
277 }
278 }
279
280 #[must_use]
282 pub fn encode(&self) -> [u8; COMMIT_MARKER_RECORD_BYTES] {
283 let mut buf = [0u8; COMMIT_MARKER_RECORD_BYTES];
284 buf[0..8].copy_from_slice(&self.commit_seq.to_le_bytes());
285 buf[8..16].copy_from_slice(&self.commit_time_unix_ns.to_le_bytes());
286 buf[16..32].copy_from_slice(&self.capsule_object_id);
287 buf[32..48].copy_from_slice(&self.proof_object_id);
288 buf[48..64].copy_from_slice(&self.prev_marker_id);
289 buf[64..80].copy_from_slice(&self.marker_id);
290 let hash = xxhash_rust::xxh3::xxh3_64(&buf[..80]);
292 buf[80..88].copy_from_slice(&hash.to_le_bytes());
293 buf
294 }
295
296 pub fn decode(data: &[u8]) -> Result<Self, MarkerError> {
298 if data.len() < COMMIT_MARKER_RECORD_BYTES {
299 return Err(MarkerError::RecordTooShort);
300 }
301
302 let commit_seq = u64::from_le_bytes(data[0..8].try_into().expect("8 bytes"));
303 let commit_time_unix_ns = u64::from_le_bytes(data[8..16].try_into().expect("8 bytes"));
304
305 let mut capsule_object_id = [0u8; ID_SIZE];
306 capsule_object_id.copy_from_slice(&data[16..32]);
307
308 let mut proof_object_id = [0u8; ID_SIZE];
309 proof_object_id.copy_from_slice(&data[32..48]);
310
311 let mut prev_marker_id = [0u8; ID_SIZE];
312 prev_marker_id.copy_from_slice(&data[48..64]);
313
314 let mut marker_id = [0u8; ID_SIZE];
315 marker_id.copy_from_slice(&data[64..80]);
316
317 let stored_hash = u64::from_le_bytes(data[80..88].try_into().expect("8 bytes"));
318 let computed_hash = xxhash_rust::xxh3::xxh3_64(&data[..80]);
319 if stored_hash != computed_hash {
320 return Err(MarkerError::RecordChecksumMismatch {
321 expected: computed_hash,
322 actual: stored_hash,
323 });
324 }
325
326 Ok(Self {
327 commit_seq,
328 commit_time_unix_ns,
329 capsule_object_id,
330 proof_object_id,
331 prev_marker_id,
332 marker_id,
333 })
334 }
335
336 #[must_use]
338 pub fn verify_marker_id(&self) -> bool {
339 let expected = compute_marker_id(
340 self.commit_seq,
341 self.commit_time_unix_ns,
342 &self.capsule_object_id,
343 &self.proof_object_id,
344 &self.prev_marker_id,
345 );
346 self.marker_id == expected
347 }
348}
349
350#[must_use]
359pub fn compute_marker_id(
360 commit_seq: u64,
361 commit_time_unix_ns: u64,
362 capsule_object_id: &[u8; ID_SIZE],
363 proof_object_id: &[u8; ID_SIZE],
364 prev_marker_id: &[u8; ID_SIZE],
365) -> [u8; ID_SIZE] {
366 let mut prefix = [0u8; RECORD_PREFIX_BYTES];
367 prefix[0..8].copy_from_slice(&commit_seq.to_le_bytes());
368 prefix[8..16].copy_from_slice(&commit_time_unix_ns.to_le_bytes());
369 prefix[16..32].copy_from_slice(capsule_object_id);
370 prefix[32..48].copy_from_slice(proof_object_id);
371 prefix[48..64].copy_from_slice(prev_marker_id);
372
373 let mut hasher = blake3::Hasher::new();
374 hasher.update(MARKER_ID_DOMAIN);
375 hasher.update(&prefix);
376 let hash = hasher.finalize();
377
378 let mut id = [0u8; ID_SIZE];
379 id.copy_from_slice(&hash.as_bytes()[..ID_SIZE]);
380 id
381}
382
383#[must_use]
389pub const fn segment_id_for_commit_seq(commit_seq: u64) -> u64 {
390 commit_seq / MARKERS_PER_SEGMENT
391}
392
393#[must_use]
395pub const fn start_commit_seq_for_segment(segment_id: u64) -> u64 {
396 segment_id * MARKERS_PER_SEGMENT
397}
398
399#[must_use]
403pub const fn record_offset(commit_seq: u64, start_commit_seq: u64) -> u64 {
404 let slot = commit_seq - start_commit_seq;
405 MARKER_SEGMENT_HEADER_BYTES as u64 + slot * COMMIT_MARKER_RECORD_BYTES as u64
406}
407
408#[must_use]
412pub const fn next_commit_seq_from_file_len(start_commit_seq: u64, file_len: u64) -> u64 {
413 if file_len < MARKER_SEGMENT_HEADER_BYTES as u64 {
414 return start_commit_seq;
415 }
416 let data_bytes = file_len - MARKER_SEGMENT_HEADER_BYTES as u64;
417 let n_records = data_bytes / COMMIT_MARKER_RECORD_BYTES as u64;
418 start_commit_seq + n_records
419}
420
421#[must_use]
430pub fn valid_record_prefix_count(data: &[u8]) -> u64 {
431 let mut count = 0u64;
432 let mut offset = 0;
433 while offset + COMMIT_MARKER_RECORD_BYTES <= data.len() {
434 let record_bytes = &data[offset..offset + COMMIT_MARKER_RECORD_BYTES];
435 if CommitMarkerRecord::decode(record_bytes).is_err() {
436 break;
437 }
438 count += 1;
439 offset += COMMIT_MARKER_RECORD_BYTES;
440 }
441 count
442}
443
444pub fn check_segment_integrity(segment_data: &[u8]) -> Result<u64, MarkerError> {
449 if segment_data.len() < MARKER_SEGMENT_HEADER_BYTES {
450 return Err(MarkerError::HeaderTooShort);
451 }
452
453 let _header = MarkerSegmentHeader::decode(&segment_data[..MARKER_SEGMENT_HEADER_BYTES])?;
455
456 let record_region = &segment_data[MARKER_SEGMENT_HEADER_BYTES..];
457 let complete_records = record_region.len() / COMMIT_MARKER_RECORD_BYTES;
458 let trailing = record_region.len() % COMMIT_MARKER_RECORD_BYTES;
459
460 let recovered = recover_valid_prefix(segment_data)?;
463 let valid = u64::try_from(recovered.len()).expect("record count always fits in u64");
464
465 #[allow(clippy::cast_possible_truncation)]
466 let complete_u64 = complete_records as u64;
467
468 if trailing > 0 || valid < complete_u64 {
469 let valid_usize = usize::try_from(valid).unwrap_or(usize::MAX);
470 return Err(MarkerError::TornTail {
471 complete_records: valid,
472 trailing_bytes: if valid < complete_u64 {
473 record_region.len() - (valid_usize * COMMIT_MARKER_RECORD_BYTES)
475 } else {
476 trailing
477 },
478 });
479 }
480
481 Ok(valid)
482}
483
484pub fn recover_valid_prefix(segment_data: &[u8]) -> Result<Vec<CommitMarkerRecord>, MarkerError> {
490 if segment_data.len() < MARKER_SEGMENT_HEADER_BYTES {
491 return Err(MarkerError::HeaderTooShort);
492 }
493
494 let header = MarkerSegmentHeader::decode(&segment_data[..MARKER_SEGMENT_HEADER_BYTES])?;
495 let record_region = &segment_data[MARKER_SEGMENT_HEADER_BYTES..];
496
497 let mut records = Vec::new();
498 let mut offset = 0usize;
499
500 while offset + COMMIT_MARKER_RECORD_BYTES <= record_region.len() {
501 let record_bytes = &record_region[offset..offset + COMMIT_MARKER_RECORD_BYTES];
502 let Ok(record) = CommitMarkerRecord::decode(record_bytes) else {
503 break;
504 };
505
506 let expected = header.start_commit_seq
507 + u64::try_from(records.len()).expect("record vector length always fits in u64");
508 if record.commit_seq != expected {
509 return Err(MarkerError::CommitSeqMismatch {
510 expected,
511 actual: record.commit_seq,
512 });
513 }
514
515 records.push(record);
516 offset += COMMIT_MARKER_RECORD_BYTES;
517 }
518
519 Ok(records)
520}
521
522pub fn binary_search_by_time<F>(
531 start_commit_seq: u64,
532 record_count: u64,
533 target_ns: u64,
534 mut read_record: F,
535) -> Option<u64>
536where
537 F: FnMut(u64) -> Option<CommitMarkerRecord>,
538{
539 if record_count == 0 {
540 return None;
541 }
542
543 let mut lo = 0u64;
544 let mut hi = record_count;
545 let mut best: Option<u64> = None;
546
547 while lo < hi {
548 let mid = lo + (hi - lo) / 2;
549 let seq = start_commit_seq + mid;
550 let Some(record) = read_record(seq) else {
551 break;
552 };
553
554 if record.commit_time_unix_ns <= target_ns {
555 best = Some(seq);
556 lo = mid + 1;
557 } else {
558 hi = mid;
559 }
560 }
561
562 best
563}
564
565#[cfg(test)]
570mod tests {
571 use super::*;
572 use std::collections::HashSet;
573
574 fn make_test_record(seq: u64, prev: [u8; ID_SIZE]) -> CommitMarkerRecord {
579 let capsule = [(seq & 0xFF) as u8; ID_SIZE];
580 let proof = [((seq >> 8) & 0xFF) as u8; ID_SIZE];
581 let time_ns = 1_700_000_000_000_000_000u64 + seq * 1_000_000;
582 CommitMarkerRecord::new(seq, time_ns, capsule, proof, prev)
583 }
584
585 #[test]
586 fn test_marker_segment_header_encode_decode() {
587 let header = MarkerSegmentHeader::new(42, 42_000_000);
588 let encoded = header.encode();
589 assert_eq!(
590 encoded.len(),
591 MARKER_SEGMENT_HEADER_BYTES,
592 "header must be exactly {MARKER_SEGMENT_HEADER_BYTES} bytes"
593 );
594
595 let decoded = MarkerSegmentHeader::decode(&encoded).expect("decode must succeed");
596 assert_eq!(decoded, header);
597
598 assert_eq!(&encoded[0..4], b"FSMK");
600 }
601
602 #[test]
603 fn test_commit_marker_record_encode_decode() {
604 let record = make_test_record(7, [0u8; ID_SIZE]);
605 let encoded = record.encode();
606 assert_eq!(
607 encoded.len(),
608 COMMIT_MARKER_RECORD_BYTES,
609 "record must be exactly {COMMIT_MARKER_RECORD_BYTES} bytes"
610 );
611
612 let decoded = CommitMarkerRecord::decode(&encoded).expect("decode must succeed");
613 assert_eq!(decoded, record);
614 }
615
616 #[test]
617 fn test_marker_id_computation() {
618 let seq = 100u64;
619 let time_ns = 1_700_000_000_000_000_000u64;
620 let capsule = [0xAAu8; ID_SIZE];
621 let proof = [0xBBu8; ID_SIZE];
622 let prev = [0u8; ID_SIZE];
623
624 let marker_id = compute_marker_id(seq, time_ns, &capsule, &proof, &prev);
625
626 let mut prefix = [0u8; RECORD_PREFIX_BYTES];
628 prefix[0..8].copy_from_slice(&seq.to_le_bytes());
629 prefix[8..16].copy_from_slice(&time_ns.to_le_bytes());
630 prefix[16..32].copy_from_slice(&capsule);
631 prefix[32..48].copy_from_slice(&proof);
632 prefix[48..64].copy_from_slice(&prev);
633
634 let mut hasher = blake3::Hasher::new();
635 hasher.update(MARKER_ID_DOMAIN);
636 hasher.update(&prefix);
637 let hash = hasher.finalize();
638 let mut expected = [0u8; ID_SIZE];
639 expected.copy_from_slice(&hash.as_bytes()[..ID_SIZE]);
640
641 assert_eq!(
642 marker_id, expected,
643 "marker_id must be Trunc128(BLAKE3(domain || prefix))"
644 );
645 }
646
647 #[test]
648 fn test_density_invariant() {
649 let start_seq = 1000u64;
650 let mut prev = [0u8; ID_SIZE];
651 let mut records = Vec::new();
652
653 for i in 0..5u64 {
654 let record = make_test_record(start_seq + i, prev);
655 prev = record.marker_id;
656 records.push(record);
657 }
658
659 for (i, record) in records.iter().enumerate() {
660 assert_eq!(
661 record.commit_seq,
662 start_seq + i as u64,
663 "record at slot {i} must have commit_seq = start + {i}"
664 );
665 }
666 }
667
668 #[test]
669 fn test_o1_seek_by_commit_seq() {
670 let start_seq = 0u64;
671 let header = MarkerSegmentHeader::new(0, start_seq);
672 let mut segment = Vec::from(header.encode());
673
674 let mut prev = [0u8; ID_SIZE];
675 let mut records = Vec::new();
676 for i in 0..1000u64 {
677 let record = make_test_record(start_seq + i, prev);
678 prev = record.marker_id;
679 segment.extend_from_slice(&record.encode());
680 records.push(record);
681 }
682
683 let target_seq = 500u64;
685 #[allow(clippy::cast_possible_truncation)]
686 let offset = record_offset(target_seq, start_seq) as usize;
687 let record_bytes = &segment[offset..offset + COMMIT_MARKER_RECORD_BYTES];
688 let record = CommitMarkerRecord::decode(record_bytes).expect("decode at offset");
689 assert_eq!(record.commit_seq, target_seq);
690 assert_eq!(record, records[500]);
691 }
692
693 #[test]
694 fn test_commit_seq_allocation_from_file_length() {
695 let start_seq = 5000u64;
696 let file_len = MARKER_SEGMENT_HEADER_BYTES as u64 + 10 * COMMIT_MARKER_RECORD_BYTES as u64;
698 assert_eq!(file_len, 916);
699
700 let next = next_commit_seq_from_file_len(start_seq, file_len);
701 assert_eq!(next, start_seq + 10);
702 }
703
704 #[test]
705 fn test_torn_tail_handling() {
706 let start_seq = 0u64;
707 let header = MarkerSegmentHeader::new(0, start_seq);
708 let mut segment = Vec::from(header.encode());
709
710 let mut prev = [0u8; ID_SIZE];
711 for i in 0..5u64 {
712 let record = make_test_record(start_seq + i, prev);
713 prev = record.marker_id;
714 segment.extend_from_slice(&record.encode());
715 }
716
717 segment.extend_from_slice(&[0xDE; 44]);
719
720 let result = check_segment_integrity(&segment);
721 match result {
722 Err(MarkerError::TornTail {
723 complete_records,
724 trailing_bytes,
725 }) => {
726 assert_eq!(complete_records, 5);
727 assert_eq!(trailing_bytes, 44);
728 }
729 other => unreachable!("expected TornTail, got {other:?}"),
730 }
731 }
732
733 #[test]
734 fn test_torn_tail_corrupt_last_record() {
735 let start_seq = 0u64;
736 let header = MarkerSegmentHeader::new(0, start_seq);
737 let mut segment = Vec::from(header.encode());
738
739 let mut prev = [0u8; ID_SIZE];
740 for i in 0..5u64 {
741 let record = make_test_record(start_seq + i, prev);
742 prev = record.marker_id;
743 segment.extend_from_slice(&record.encode());
744 }
745
746 let record_4_end = MARKER_SEGMENT_HEADER_BYTES + 5 * COMMIT_MARKER_RECORD_BYTES;
748 let xxh3_start = record_4_end - 8;
749 segment[xxh3_start] ^= 0xFF;
750
751 let result = check_segment_integrity(&segment);
752 match result {
753 Err(MarkerError::TornTail {
754 complete_records, ..
755 }) => {
756 assert_eq!(complete_records, 4, "valid prefix is records 0-3");
757 }
758 other => unreachable!("expected TornTail, got {other:?}"),
759 }
760 }
761
762 #[test]
763 fn test_commit_seq_mismatch_detected() {
764 let start_seq = 100u64;
765 let header = MarkerSegmentHeader::new(0, start_seq);
766 let mut segment = Vec::from(header.encode());
767
768 let first = make_test_record(start_seq, [0u8; ID_SIZE]);
769 let second = make_test_record(start_seq + 2, first.marker_id);
770 segment.extend_from_slice(&first.encode());
771 segment.extend_from_slice(&second.encode());
772
773 let result = check_segment_integrity(&segment);
774 match result {
775 Err(MarkerError::CommitSeqMismatch { expected, actual }) => {
776 assert_eq!(expected, start_seq + 1);
777 assert_eq!(actual, start_seq + 2);
778 }
779 other => unreachable!("expected CommitSeqMismatch, got {other:?}"),
780 }
781 }
782
783 #[test]
784 fn test_binary_search_by_time() {
785 let start_seq = 0u64;
786 let base_ns = 1_000_000_000_000_000_000u64;
787
788 let records: Vec<CommitMarkerRecord> = (0..100u64)
789 .scan([0u8; ID_SIZE], |prev, i| {
790 let capsule = [(i & 0xFF) as u8; ID_SIZE];
791 let proof = [((i >> 8) & 0xFF) as u8; ID_SIZE];
792 let time_ns = base_ns + i * 1_000_000;
793 let record = CommitMarkerRecord::new(i, time_ns, capsule, proof, *prev);
794 *prev = record.marker_id;
795 Some(record)
796 })
797 .collect();
798
799 let target_ns = base_ns + 50 * 1_000_000;
801 #[allow(clippy::cast_possible_truncation)]
802 let result = binary_search_by_time(start_seq, 100, target_ns, |seq| {
803 records.get(seq as usize).cloned()
804 });
805 assert_eq!(result, Some(50));
806
807 #[allow(clippy::cast_possible_truncation)]
809 let result = binary_search_by_time(start_seq, 100, base_ns - 1, |seq| {
810 records.get(seq as usize).cloned()
811 });
812 assert_eq!(result, None);
813
814 #[allow(clippy::cast_possible_truncation)]
816 let result = binary_search_by_time(start_seq, 100, u64::MAX, |seq| {
817 records.get(seq as usize).cloned()
818 });
819 assert_eq!(result, Some(99));
820 }
821
822 #[test]
823 fn test_fork_detection() {
824 let base_ns = 1_700_000_000_000_000_000u64;
825 let mut prev = [0u8; ID_SIZE];
826
827 let mut shared = Vec::new();
829 for i in 0..10u64 {
830 let capsule = [0xAAu8; ID_SIZE];
831 let proof = [0xBBu8; ID_SIZE];
832 let record = CommitMarkerRecord::new(i, base_ns + i * 1_000_000, capsule, proof, prev);
833 prev = record.marker_id;
834 shared.push(record);
835 }
836
837 let mut fork_a = shared.clone();
839 let mut prev_a = shared[9].marker_id;
840 for i in 10..15u64 {
841 let capsule = [0x11u8; ID_SIZE];
842 let proof = [0x22u8; ID_SIZE];
843 let record =
844 CommitMarkerRecord::new(i, base_ns + i * 1_000_000, capsule, proof, prev_a);
845 prev_a = record.marker_id;
846 fork_a.push(record);
847 }
848
849 let mut fork_b = shared.clone();
851 let mut prev_b = shared[9].marker_id;
852 for i in 10..13u64 {
853 let capsule = [0x33u8; ID_SIZE];
854 let proof = [0x44u8; ID_SIZE];
855 let record =
856 CommitMarkerRecord::new(i, base_ns + i * 1_000_000, capsule, proof, prev_b);
857 prev_b = record.marker_id;
858 fork_b.push(record);
859 }
860
861 let min_len = fork_a.len().min(fork_b.len());
863 let mut lo = 0usize;
864 let mut hi = min_len;
865 while lo < hi {
866 let mid = lo + (hi - lo) / 2;
867 if fork_a[mid].marker_id == fork_b[mid].marker_id {
868 lo = mid + 1;
869 } else {
870 hi = mid;
871 }
872 }
873
874 assert_eq!(lo, 10, "divergence starts at commit_seq 10");
876 assert_eq!(
877 fork_a[9].marker_id, fork_b[9].marker_id,
878 "last matching marker_id is at seq 9"
879 );
880 assert_ne!(
881 fork_a[10].marker_id, fork_b[10].marker_id,
882 "first divergence at seq 10"
883 );
884 }
885
886 #[test]
887 fn test_hash_chain_integrity() {
888 let mut prev = [0u8; ID_SIZE];
889 let mut records = Vec::new();
890
891 for i in 0..10u64 {
892 let record = make_test_record(i, prev);
893 prev = record.marker_id;
894 records.push(record);
895 }
896
897 for i in 1..records.len() {
899 assert_eq!(
900 records[i].prev_marker_id,
901 records[i - 1].marker_id,
902 "record {i} prev_marker_id must link to record {}'s marker_id",
903 i - 1
904 );
905 }
906 assert_eq!(records[0].prev_marker_id, [0u8; ID_SIZE], "genesis is zero");
907
908 for record in &records {
910 assert!(
911 record.verify_marker_id(),
912 "marker_id must be verifiable for commit_seq {}",
913 record.commit_seq
914 );
915 }
916
917 let mut tampered = records[5].clone();
919 tampered.capsule_object_id[0] ^= 0xFF;
920 assert!(
921 !tampered.verify_marker_id(),
922 "tampered record must fail marker_id verification"
923 );
924 }
925
926 #[test]
927 fn test_marker_no_mem_size_of() {
928 assert_eq!(MARKER_SEGMENT_HEADER_BYTES, 36);
930 assert_eq!(COMMIT_MARKER_RECORD_BYTES, 88);
931
932 let header = MarkerSegmentHeader::new(0, 0);
935 let encoded_header = header.encode();
936 assert_eq!(
937 encoded_header.len(),
938 36,
939 "header on-disk size is a constant"
940 );
941
942 let record = make_test_record(0, [0u8; ID_SIZE]);
943 let encoded_record = record.encode();
944 assert_eq!(
945 encoded_record.len(),
946 88,
947 "record on-disk size is a constant"
948 );
949 }
950
951 #[test]
952 fn test_header_bad_magic_rejected() {
953 let header = MarkerSegmentHeader::new(1, 0);
954 let mut encoded = header.encode();
955 encoded[0] = b'X';
956
957 let result = MarkerSegmentHeader::decode(&encoded);
958 assert_eq!(result.unwrap_err(), MarkerError::BadMagic);
959 }
960
961 #[test]
962 fn test_header_checksum_tamper_detected() {
963 let header = MarkerSegmentHeader::new(1, 0);
964 let mut encoded = header.encode();
965 encoded[8] ^= 0x01;
967
968 let result = MarkerSegmentHeader::decode(&encoded);
969 assert!(matches!(
970 result.unwrap_err(),
971 MarkerError::HeaderChecksumMismatch { .. }
972 ));
973 }
974
975 #[test]
976 fn test_record_checksum_tamper_detected() {
977 let record = make_test_record(42, [0u8; ID_SIZE]);
978 let mut encoded = record.encode();
979 encoded[10] ^= 0x01;
981
982 let result = CommitMarkerRecord::decode(&encoded);
983 assert!(matches!(
984 result.unwrap_err(),
985 MarkerError::RecordChecksumMismatch { .. }
986 ));
987 }
988
989 #[test]
990 fn test_segment_id_for_commit_seq() {
991 assert_eq!(segment_id_for_commit_seq(0), 0);
992 assert_eq!(segment_id_for_commit_seq(999_999), 0);
993 assert_eq!(segment_id_for_commit_seq(1_000_000), 1);
994 assert_eq!(segment_id_for_commit_seq(2_500_000), 2);
995 }
996
997 #[test]
998 fn test_start_commit_seq_for_segment() {
999 assert_eq!(start_commit_seq_for_segment(0), 0);
1000 assert_eq!(start_commit_seq_for_segment(1), 1_000_000);
1001 assert_eq!(start_commit_seq_for_segment(5), 5_000_000);
1002 }
1003
1004 #[test]
1005 fn test_record_offset_formula() {
1006 let offset = record_offset(500, 0);
1007 assert_eq!(
1008 offset,
1009 MARKER_SEGMENT_HEADER_BYTES as u64 + 500 * COMMIT_MARKER_RECORD_BYTES as u64
1010 );
1011
1012 let offset2 = record_offset(1_000_050, 1_000_000);
1013 assert_eq!(
1014 offset2,
1015 MARKER_SEGMENT_HEADER_BYTES as u64 + 50 * COMMIT_MARKER_RECORD_BYTES as u64
1016 );
1017 }
1018
1019 #[test]
1020 fn test_error_display() {
1021 let err = MarkerError::BadMagic;
1022 assert_eq!(err.to_string(), "bad magic in marker segment header");
1023
1024 let err = MarkerError::TornTail {
1025 complete_records: 5,
1026 trailing_bytes: 44,
1027 };
1028 assert!(err.to_string().contains("torn tail"));
1029 assert!(err.to_string().contains('5'));
1030 }
1031
1032 #[test]
1033 fn test_marker_id_deterministic() {
1034 let capsule = [0xAA; ID_SIZE];
1035 let proof = [0xBB; ID_SIZE];
1036 let prev = [0u8; ID_SIZE];
1037
1038 let id1 = compute_marker_id(1, 100, &capsule, &proof, &prev);
1039 let id2 = compute_marker_id(1, 100, &capsule, &proof, &prev);
1040 assert_eq!(id1, id2, "marker_id must be deterministic");
1041
1042 let id3 = compute_marker_id(2, 100, &capsule, &proof, &prev);
1044 assert_ne!(id1, id3);
1045 }
1046
1047 #[test]
1048 fn prop_marker_id_unique() {
1049 let mut rng_state = 0xDEAD_BEEF_CAFE_BABE_u64;
1050 let mut observed_ids = HashSet::new();
1051
1052 for i in 0..2048u64 {
1053 rng_state = rng_state
1055 .wrapping_mul(6_364_136_223_846_793_005)
1056 .wrapping_add(1);
1057 let mut capsule = [0u8; ID_SIZE];
1058 let mut proof = [0u8; ID_SIZE];
1059 let mut prev = [0u8; ID_SIZE];
1060
1061 capsule[..8].copy_from_slice(&rng_state.to_le_bytes());
1062 proof[..8].copy_from_slice(&rng_state.rotate_left(13).to_le_bytes());
1063 prev[..8].copy_from_slice(&rng_state.rotate_right(7).to_le_bytes());
1064 capsule[8..16].copy_from_slice(&i.to_le_bytes());
1065 proof[8..16].copy_from_slice(&(i ^ rng_state).to_le_bytes());
1066 prev[8..16].copy_from_slice(&(i.wrapping_mul(17)).to_le_bytes());
1067
1068 let marker_id =
1069 compute_marker_id(i, 1_700_000_000_000_000_000 + i, &capsule, &proof, &prev);
1070 assert!(
1071 observed_ids.insert(marker_id),
1072 "marker_id collision at sample {i}: {marker_id:02X?}"
1073 );
1074 }
1075 }
1076
1077 #[test]
1078 fn prop_density_invariant_holds() {
1079 for count in 1..=256u64 {
1080 let start_seq = 10_000 + count * 31;
1081 let header = MarkerSegmentHeader::new(segment_id_for_commit_seq(start_seq), start_seq);
1082 let mut segment = Vec::from(header.encode());
1083 let mut prev = [0u8; ID_SIZE];
1084
1085 for i in 0..count {
1086 let record = make_test_record(start_seq + i, prev);
1087 prev = record.marker_id;
1088 segment.extend_from_slice(&record.encode());
1089 }
1090
1091 let integrity = check_segment_integrity(&segment).expect("segment should be dense");
1092 assert_eq!(integrity, count);
1093 }
1094 }
1095
1096 #[test]
1097 fn test_e2e_marker_stream_recovery() {
1098 let start_seq = 20_000u64;
1099 let header = MarkerSegmentHeader::new(segment_id_for_commit_seq(start_seq), start_seq);
1100 let mut segment = Vec::from(header.encode());
1101 let mut expected = Vec::new();
1102 let mut prev = [0u8; ID_SIZE];
1103
1104 for i in 0..1000u64 {
1105 let record = make_test_record(start_seq + i, prev);
1106 prev = record.marker_id;
1107 segment.extend_from_slice(&record.encode());
1108 expected.push(record);
1109 }
1110
1111 segment.truncate(segment.len() - (COMMIT_MARKER_RECORD_BYTES / 2));
1113
1114 let recovered = recover_valid_prefix(&segment).expect("recovery should succeed");
1115 assert_eq!(recovered.len(), expected.len() - 1);
1116 assert_eq!(recovered, expected[..expected.len() - 1]);
1117
1118 let integrity = check_segment_integrity(&segment);
1119 match integrity {
1120 Err(MarkerError::TornTail {
1121 complete_records,
1122 trailing_bytes,
1123 }) => {
1124 assert_eq!(complete_records, 999);
1125 assert_eq!(trailing_bytes, COMMIT_MARKER_RECORD_BYTES / 2);
1126 }
1127 other => unreachable!("expected torn-tail integrity result, got {other:?}"),
1128 }
1129 }
1130
1131 #[test]
1132 fn test_e2e_time_travel_query() {
1133 let start_seq = 5_000u64;
1134 let count = 256u64;
1135 let base_ns = 1_900_000_000_000_000_000u64;
1136 let mut prev = [0u8; ID_SIZE];
1137 let mut records = Vec::new();
1138
1139 for i in 0..count {
1140 let capsule = [(i & 0xFF) as u8; ID_SIZE];
1141 let proof = [((i >> 8) & 0xFF) as u8; ID_SIZE];
1142 let record = CommitMarkerRecord::new(
1143 start_seq + i,
1144 base_ns + i * 2_000_000,
1145 capsule,
1146 proof,
1147 prev,
1148 );
1149 prev = record.marker_id;
1150 records.push(record);
1151 }
1152
1153 let lookup = |seq: u64| -> Option<CommitMarkerRecord> {
1154 if seq < start_seq {
1155 return None;
1156 }
1157 let idx = usize::try_from(seq - start_seq).ok()?;
1158 records.get(idx).cloned()
1159 };
1160
1161 assert_eq!(
1163 binary_search_by_time(start_seq, count, base_ns - 1, lookup),
1164 None
1165 );
1166 assert_eq!(
1168 binary_search_by_time(start_seq, count, base_ns + 40 * 2_000_000, lookup),
1169 Some(start_seq + 40)
1170 );
1171 assert_eq!(
1173 binary_search_by_time(
1174 start_seq,
1175 count,
1176 base_ns + 40 * 2_000_000 + 1_000_000,
1177 lookup
1178 ),
1179 Some(start_seq + 40)
1180 );
1181 assert_eq!(
1183 binary_search_by_time(start_seq, count, u64::MAX, lookup),
1184 Some(start_seq + count - 1)
1185 );
1186 }
1187}