1use std::fmt;
12
13use fsqlite_error::{FrankenError, Result};
14use tracing::{debug, error, info, warn};
15
16use crate::source_block_partition::K_MAX;
17
18const BEAD_ID: &str = "bd-1hi.13";
19
20pub const CHANGESET_MAGIC: [u8; 4] = *b"FSRP";
26
27pub const CHANGESET_VERSION: u16 = 1;
29
30pub const CHANGESET_DOMAIN: &str = "fsqlite:replication:changeset:v1";
32
33pub const REPLICATION_HEADER_SIZE: usize = 72;
35
36pub const REPLICATION_HEADER_SIZE_LEGACY: usize = 24;
38
39pub const REPLICATION_PROTOCOL_MAGIC: [u8; 4] = *b"FSRP";
41
42pub const REPLICATION_PROTOCOL_VERSION_V2: u8 = 2;
44
45pub const REPLICATION_HEADER_SIZE_V2: usize = REPLICATION_HEADER_SIZE;
47pub const REPLICATION_HEADER_SIZE_V2_U16: u16 = 72;
49
50pub const REPLICATION_FLAG_AUTH_PRESENT: u8 = 0b0000_0001;
52
53pub const REPLICATION_PACKET_AUTH_DOMAIN: &str = "fsqlite:replication:packet-auth:v1";
55
56pub const MAX_UDP_PAYLOAD: usize = 65_507;
58
59pub const MAX_REPLICATION_SYMBOL_SIZE: usize = MAX_UDP_PAYLOAD - REPLICATION_HEADER_SIZE;
61
62pub const MTU_SAFE_SYMBOL_SIZE: u16 = 1400;
65
66pub const DEFAULT_MAX_ISI_MULTIPLIER: u32 = 2;
68
69pub const DEFAULT_RPC_MESSAGE_CAP_BYTES: usize = 4 * 1024 * 1024;
71
72pub const DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS: u32 = 256;
74
75pub const DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE: usize = 65_536;
77
78pub const DEFAULT_HTTP2_CONTINUATION_TIMEOUT_MS: u64 = 5_000;
80
81pub const DEFAULT_HTTP2_HEADER_FRAGMENT_CAP: usize = 262_144;
83
84pub const DEFAULT_HANDSHAKE_TIMEOUT_MS: u64 = 500;
86
87pub const CHANGESET_HEADER_SIZE: usize = 4 + 2 + 4 + 4 + 8; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum TransportSecurityMode {
97 RustlsTls,
99 Plaintext,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct Http2HardLimits {
106 pub max_concurrent_streams: u32,
107 pub max_header_list_size: usize,
108 pub continuation_timeout_ms: u64,
109 pub header_fragment_cap: usize,
110}
111
112impl Default for Http2HardLimits {
113 fn default() -> Self {
114 Self {
115 max_concurrent_streams: DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS,
116 max_header_list_size: DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE,
117 continuation_timeout_ms: DEFAULT_HTTP2_CONTINUATION_TIMEOUT_MS,
118 header_fragment_cap: DEFAULT_HTTP2_HEADER_FRAGMENT_CAP,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct NetworkStackConfig {
126 pub security: TransportSecurityMode,
127 pub explicit_plaintext_opt_in: bool,
128 pub handshake_timeout_ms: u64,
129 pub message_size_cap_bytes: usize,
130 pub http2: Http2HardLimits,
131}
132
133impl Default for NetworkStackConfig {
134 fn default() -> Self {
135 Self {
136 security: TransportSecurityMode::RustlsTls,
137 explicit_plaintext_opt_in: false,
138 handshake_timeout_ms: DEFAULT_HANDSHAKE_TIMEOUT_MS,
139 message_size_cap_bytes: DEFAULT_RPC_MESSAGE_CAP_BYTES,
140 http2: Http2HardLimits::default(),
141 }
142 }
143}
144
145impl NetworkStackConfig {
146 pub fn plaintext_local_dev(explicit_opt_in: bool) -> Result<Self> {
153 if !explicit_opt_in {
154 return Err(FrankenError::Unsupported);
155 }
156 Ok(Self {
157 security: TransportSecurityMode::Plaintext,
158 explicit_plaintext_opt_in: true,
159 ..Self::default()
160 })
161 }
162
163 pub fn validate_security(&self) -> Result<()> {
169 if self.security == TransportSecurityMode::Plaintext && !self.explicit_plaintext_opt_in {
170 return Err(FrankenError::Unsupported);
171 }
172 Ok(())
173 }
174
175 pub fn validate_concurrent_streams(&self, streams: u32) -> Result<()> {
181 if streams > self.http2.max_concurrent_streams {
182 return Err(FrankenError::Busy);
183 }
184 Ok(())
185 }
186
187 pub fn validate_header_list_size(&self, header_bytes: usize) -> Result<()> {
193 if header_bytes > self.http2.max_header_list_size {
194 return Err(FrankenError::TooBig);
195 }
196 Ok(())
197 }
198
199 pub fn validate_continuation_elapsed(&self, elapsed_ms: u64) -> Result<()> {
206 if elapsed_ms > self.http2.continuation_timeout_ms {
207 return Err(FrankenError::BusyRecovery);
208 }
209 Ok(())
210 }
211
212 pub fn validate_handshake_elapsed(&self, elapsed_ms: u64) -> Result<()> {
218 if elapsed_ms > self.handshake_timeout_ms {
219 return Err(FrankenError::BusyRecovery);
220 }
221 Ok(())
222 }
223
224 pub fn validate_message_size(&self, message_bytes: usize) -> Result<()> {
230 if message_bytes > self.message_size_cap_bytes {
231 return Err(FrankenError::TooBig);
232 }
233 Ok(())
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub struct VirtualTcpFaultProfile {
240 pub drop_per_million: u32,
241 pub reorder_per_million: u32,
242 pub corrupt_per_million: u32,
243}
244
245impl VirtualTcpFaultProfile {
246 pub fn validate(&self) -> Result<()> {
252 const PPM_MAX: u32 = 1_000_000;
253 if self.drop_per_million > PPM_MAX {
254 return Err(FrankenError::OutOfRange {
255 what: "drop_per_million".to_owned(),
256 value: self.drop_per_million.to_string(),
257 });
258 }
259 if self.reorder_per_million > PPM_MAX {
260 return Err(FrankenError::OutOfRange {
261 what: "reorder_per_million".to_owned(),
262 value: self.reorder_per_million.to_string(),
263 });
264 }
265 if self.corrupt_per_million > PPM_MAX {
266 return Err(FrankenError::OutOfRange {
267 what: "corrupt_per_million".to_owned(),
268 value: self.corrupt_per_million.to_string(),
269 });
270 }
271 Ok(())
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum VirtualTcpTraceKind {
278 Dropped,
279 BufferedForReorder,
280 Delivered,
281 DeliveredCorrupt,
282 FlushedReordered,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct VirtualTcpTraceEvent {
288 pub seq: u64,
289 pub kind: VirtualTcpTraceKind,
290 pub payload_hash: u64,
291}
292
293#[derive(Debug, Clone)]
295pub struct VirtualTcp {
296 state: u64,
297 seq: u64,
298 faults: VirtualTcpFaultProfile,
299 pending_reorder: Option<Vec<u8>>,
300 trace: Vec<VirtualTcpTraceEvent>,
301}
302
303impl VirtualTcp {
304 pub fn new(seed: u64, faults: VirtualTcpFaultProfile) -> Result<Self> {
310 faults.validate()?;
311 Ok(Self {
312 state: seed,
313 seq: 0,
314 faults,
315 pending_reorder: None,
316 trace: Vec::new(),
317 })
318 }
319
320 #[must_use]
322 pub fn trace(&self) -> &[VirtualTcpTraceEvent] {
323 &self.trace
324 }
325
326 #[must_use]
330 pub fn transmit(&mut self, payload: &[u8]) -> Vec<Vec<u8>> {
331 self.seq = self.seq.saturating_add(1);
332
333 if self.coin_flip(self.faults.drop_per_million) {
334 self.push_trace(VirtualTcpTraceKind::Dropped, payload);
335 return Vec::new();
336 }
337
338 let mut wire = payload.to_vec();
339 let corrupted = if !wire.is_empty() && self.coin_flip(self.faults.corrupt_per_million) {
340 let idx = (self.next_u32() as usize) % wire.len();
341 wire[idx] ^= 0x01;
342 true
343 } else {
344 false
345 };
346
347 if self.coin_flip(self.faults.reorder_per_million) && self.pending_reorder.is_none() {
348 self.push_trace(VirtualTcpTraceKind::BufferedForReorder, &wire);
349 self.pending_reorder = Some(wire);
350 return Vec::new();
351 }
352
353 let mut out = Vec::with_capacity(2);
354 if let Some(previous) = self.pending_reorder.take() {
355 let kind = if corrupted {
356 VirtualTcpTraceKind::DeliveredCorrupt
357 } else {
358 VirtualTcpTraceKind::Delivered
359 };
360 self.push_trace(kind, &wire);
361 out.push(wire);
362 self.push_trace(VirtualTcpTraceKind::FlushedReordered, &previous);
363 out.push(previous);
364 return out;
365 }
366
367 let kind = if corrupted {
368 VirtualTcpTraceKind::DeliveredCorrupt
369 } else {
370 VirtualTcpTraceKind::Delivered
371 };
372 self.push_trace(kind, &wire);
373 out.push(wire);
374 out
375 }
376
377 pub fn flush(&mut self) -> Option<Vec<u8>> {
379 let pending = self.pending_reorder.take()?;
380 self.seq = self.seq.saturating_add(1);
381 self.push_trace(VirtualTcpTraceKind::FlushedReordered, &pending);
382 Some(pending)
383 }
384
385 fn push_trace(&mut self, kind: VirtualTcpTraceKind, payload: &[u8]) {
386 self.trace.push(VirtualTcpTraceEvent {
387 seq: self.seq,
388 kind,
389 payload_hash: xxhash_rust::xxh3::xxh3_64(payload),
390 });
391 }
392
393 fn coin_flip(&mut self, per_million: u32) -> bool {
394 const PPM_MAX: u32 = 1_000_000;
395 if per_million == 0 {
396 return false;
397 }
398 if per_million >= PPM_MAX {
399 return true;
400 }
401 self.next_u32() % PPM_MAX < per_million
402 }
403
404 fn next_u32(&mut self) -> u32 {
405 self.state = self
407 .state
408 .wrapping_mul(6_364_136_223_846_793_005)
409 .wrapping_add(1);
410 (self.state >> 32) as u32
411 }
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct ChangesetHeader {
421 pub magic: [u8; 4],
422 pub version: u16,
423 pub page_size: u32,
424 pub n_pages: u32,
425 pub total_len: u64,
426}
427
428impl ChangesetHeader {
429 #[must_use]
431 pub fn to_bytes(&self) -> [u8; CHANGESET_HEADER_SIZE] {
432 let mut buf = [0_u8; CHANGESET_HEADER_SIZE];
433 buf[0..4].copy_from_slice(&self.magic);
434 buf[4..6].copy_from_slice(&self.version.to_le_bytes());
435 buf[6..10].copy_from_slice(&self.page_size.to_le_bytes());
436 buf[10..14].copy_from_slice(&self.n_pages.to_le_bytes());
437 buf[14..22].copy_from_slice(&self.total_len.to_le_bytes());
438 buf
439 }
440
441 pub fn from_bytes(buf: &[u8; CHANGESET_HEADER_SIZE]) -> Result<Self> {
447 let magic: [u8; 4] = buf[0..4].try_into().expect("4 bytes");
448 if magic != CHANGESET_MAGIC {
449 return Err(FrankenError::DatabaseCorrupt {
450 detail: format!("changeset magic mismatch: expected FSRP, got {magic:?}"),
451 });
452 }
453 let version = u16::from_le_bytes(buf[4..6].try_into().expect("2 bytes"));
454 if version != CHANGESET_VERSION {
455 return Err(FrankenError::DatabaseCorrupt {
456 detail: format!(
457 "changeset version mismatch: expected {CHANGESET_VERSION}, got {version}"
458 ),
459 });
460 }
461 let page_size = u32::from_le_bytes(buf[6..10].try_into().expect("4 bytes"));
462 let n_pages = u32::from_le_bytes(buf[10..14].try_into().expect("4 bytes"));
463 let total_len = u64::from_le_bytes(buf[14..22].try_into().expect("8 bytes"));
464 Ok(Self {
465 magic,
466 version,
467 page_size,
468 n_pages,
469 total_len,
470 })
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
476pub struct PageEntry {
477 pub page_number: u32,
478 pub page_xxh3: u64,
479 pub page_bytes: Vec<u8>,
480}
481
482impl PageEntry {
483 #[must_use]
485 pub fn new(page_number: u32, page_bytes: Vec<u8>) -> Self {
486 let page_xxh3 = xxhash_rust::xxh3::xxh3_64(&page_bytes);
487 Self {
488 page_number,
489 page_xxh3,
490 page_bytes,
491 }
492 }
493
494 #[must_use]
496 pub fn validate_xxh3(&self) -> bool {
497 xxhash_rust::xxh3::xxh3_64(&self.page_bytes) == self.page_xxh3
498 }
499}
500
501fn auth_tags_equal(lhs: &[u8; 16], rhs: &[u8; 16]) -> bool {
502 lhs.iter()
503 .zip(rhs.iter())
504 .fold(0_u8, |acc, (&left, &right)| acc | (left ^ right))
505 == 0
506}
507
508#[derive(Clone, Copy, PartialEq, Eq, Hash)]
510pub struct ChangesetId([u8; 16]);
511
512impl ChangesetId {
513 #[must_use]
515 pub const fn as_bytes(&self) -> &[u8; 16] {
516 &self.0
517 }
518
519 #[must_use]
521 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
522 Self(bytes)
523 }
524}
525
526impl fmt::Debug for ChangesetId {
527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528 write!(f, "ChangesetId(")?;
529 for byte in &self.0 {
530 write!(f, "{byte:02x}")?;
531 }
532 write!(f, ")")
533 }
534}
535
536#[must_use]
538pub fn compute_changeset_id(changeset_bytes: &[u8]) -> ChangesetId {
539 let mut hasher = blake3::Hasher::new();
540 hasher.update(CHANGESET_DOMAIN.as_bytes());
541 hasher.update(changeset_bytes);
542 let hash = hasher.finalize();
543 let mut id = [0_u8; 16];
544 id.copy_from_slice(&hash.as_bytes()[..16]);
545 ChangesetId(id)
546}
547
548#[must_use]
550pub fn derive_seed_from_changeset_id(id: &ChangesetId) -> u64 {
551 xxhash_rust::xxh3::xxh3_64(id.as_bytes())
552}
553
554#[allow(clippy::cast_possible_truncation)]
558pub(crate) fn generate_deterministic_placeholder(seed: u64, isi: u32, t: usize) -> Vec<u8> {
559 let repair_seed = seed.wrapping_add(u64::from(isi));
560 let mut data = vec![0_u8; t];
561 for (i, byte) in data.iter_mut().enumerate() {
562 let mixed = repair_seed
563 .wrapping_mul(0x9E37_79B9_7F4A_7C15)
564 .wrapping_add(i as u64);
565 *byte = (mixed >> 32) as u8;
566 }
567 data
568}
569
570pub fn compute_k_source(total_bytes: usize, symbol_size: u16) -> Result<u64> {
578 if symbol_size == 0 {
579 return Err(FrankenError::OutOfRange {
580 what: "symbol_size".to_owned(),
581 value: "0".to_owned(),
582 });
583 }
584 let f = u64::try_from(total_bytes).map_err(|_| FrankenError::OutOfRange {
585 what: "total_bytes".to_owned(),
586 value: total_bytes.to_string(),
587 })?;
588 let t = u64::from(symbol_size);
589 Ok(f.div_ceil(t))
590}
591
592fn canonicalize_changeset_pages(pages: &mut [PageEntry]) {
598 pages.sort_by(|lhs, rhs| {
599 lhs.page_number
600 .cmp(&rhs.page_number)
601 .then_with(|| lhs.page_xxh3.cmp(&rhs.page_xxh3))
602 .then_with(|| lhs.page_bytes.cmp(&rhs.page_bytes))
603 });
604}
605
606pub fn encode_changeset(page_size: u32, pages: &mut [PageEntry]) -> Result<Vec<u8>> {
619 if pages.is_empty() {
620 return Err(FrankenError::OutOfRange {
621 what: "pages".to_owned(),
622 value: "0".to_owned(),
623 });
624 }
625 if page_size == 0 {
626 return Err(FrankenError::OutOfRange {
627 what: "page_size".to_owned(),
628 value: "0".to_owned(),
629 });
630 }
631 let page_size_usize = usize::try_from(page_size).map_err(|_| FrankenError::OutOfRange {
632 what: "page_size".to_owned(),
633 value: page_size.to_string(),
634 })?;
635
636 for (index, page) in pages.iter().enumerate() {
637 if page.page_bytes.len() != page_size_usize {
638 return Err(FrankenError::OutOfRange {
639 what: format!("pages[{index}].page_bytes.len"),
640 value: format!("{} (expected {page_size_usize})", page.page_bytes.len()),
641 });
642 }
643 if !page.validate_xxh3() {
644 return Err(FrankenError::DatabaseCorrupt {
645 detail: format!(
646 "page {} xxh3 mismatch before changeset encoding",
647 page.page_number
648 ),
649 });
650 }
651 }
652
653 canonicalize_changeset_pages(pages);
654
655 let n_pages = u32::try_from(pages.len()).map_err(|_| FrankenError::OutOfRange {
656 what: "n_pages".to_owned(),
657 value: pages.len().to_string(),
658 })?;
659
660 let entry_size = 4_u64
662 .checked_add(8)
663 .and_then(|value| value.checked_add(u64::from(page_size)))
664 .ok_or_else(|| FrankenError::OutOfRange {
665 what: "entry_size".to_owned(),
666 value: format!("page_size={page_size}"),
667 })?;
668 let payload_len =
669 entry_size
670 .checked_mul(u64::from(n_pages))
671 .ok_or_else(|| FrankenError::OutOfRange {
672 what: "changeset payload size".to_owned(),
673 value: format!("entry_size={entry_size}, n_pages={n_pages}"),
674 })?;
675 let total_len = (CHANGESET_HEADER_SIZE as u64)
676 .checked_add(payload_len)
677 .ok_or_else(|| FrankenError::OutOfRange {
678 what: "changeset total_len".to_owned(),
679 value: format!("payload_len={payload_len}"),
680 })?;
681
682 let header = ChangesetHeader {
683 magic: CHANGESET_MAGIC,
684 version: CHANGESET_VERSION,
685 page_size,
686 n_pages,
687 total_len,
688 };
689
690 let buf_cap = usize::try_from(total_len).map_err(|_| FrankenError::OutOfRange {
691 what: "changeset total_len".to_owned(),
692 value: total_len.to_string(),
693 })?;
694 let mut buf = Vec::with_capacity(buf_cap);
695 buf.extend_from_slice(&header.to_bytes());
696
697 for page in pages.iter() {
698 buf.extend_from_slice(&page.page_number.to_le_bytes());
699 buf.extend_from_slice(&page.page_xxh3.to_le_bytes());
700 buf.extend_from_slice(&page.page_bytes);
701 }
702
703 debug!(
704 bead_id = BEAD_ID,
705 n_pages, page_size, total_len, "encoded changeset"
706 );
707
708 debug_assert_eq!(buf.len() as u64, total_len);
709 Ok(buf)
710}
711
712#[derive(Debug, Clone)]
718pub struct ChangesetShard {
719 pub changeset_bytes: Vec<u8>,
721 pub changeset_id: ChangesetId,
723 pub seed: u64,
725 pub k_source: u32,
727}
728
729pub fn shard_changeset(changeset_bytes: Vec<u8>, symbol_size: u16) -> Result<Vec<ChangesetShard>> {
742 let t = u64::from(symbol_size);
743 let f = u64::try_from(changeset_bytes.len()).map_err(|_| FrankenError::OutOfRange {
744 what: "changeset_bytes".to_owned(),
745 value: changeset_bytes.len().to_string(),
746 })?;
747 let k_source_total = compute_k_source(changeset_bytes.len(), symbol_size)?;
748
749 if k_source_total <= u64::from(K_MAX) {
750 let id = compute_changeset_id(&changeset_bytes);
751 let seed = derive_seed_from_changeset_id(&id);
752 let k_source = u32::try_from(k_source_total).expect("checked <= K_MAX");
753 info!(
754 bead_id = BEAD_ID,
755 k_source,
756 symbol_size,
757 changeset_len = changeset_bytes.len(),
758 "single-shard changeset"
759 );
760 return Ok(vec![ChangesetShard {
761 changeset_bytes,
762 changeset_id: id,
763 seed,
764 k_source,
765 }]);
766 }
767
768 let max_chunk = u64::from(K_MAX) * t;
771 let n_shards = f.div_ceil(max_chunk);
772
773 info!(
774 bead_id = BEAD_ID,
775 n_shards,
776 k_source_total,
777 symbol_size,
778 changeset_len = changeset_bytes.len(),
779 "sharding large changeset"
780 );
781
782 let n_shards_usize = usize::try_from(n_shards).map_err(|_| FrankenError::OutOfRange {
783 what: "n_shards".to_owned(),
784 value: n_shards.to_string(),
785 })?;
786 let mut shards = Vec::with_capacity(n_shards_usize);
787 let max_chunk_usize = usize::try_from(max_chunk).map_err(|_| FrankenError::OutOfRange {
788 what: "max_chunk".to_owned(),
789 value: max_chunk.to_string(),
790 })?;
791
792 for (i, chunk) in changeset_bytes.chunks(max_chunk_usize).enumerate() {
793 let shard_bytes = chunk.to_vec();
794 let id = compute_changeset_id(&shard_bytes);
795 let seed = derive_seed_from_changeset_id(&id);
796 let k = compute_k_source(chunk.len(), symbol_size)?;
797 let k_source = u32::try_from(k).expect("each shard <= K_MAX symbols");
798
799 debug!(
800 bead_id = BEAD_ID,
801 shard_index = i,
802 k_source,
803 shard_len = chunk.len(),
804 "created changeset shard"
805 );
806
807 shards.push(ChangesetShard {
808 changeset_bytes: shard_bytes,
809 changeset_id: id,
810 seed,
811 k_source,
812 });
813 }
814
815 Ok(shards)
816}
817
818#[derive(Debug, Clone, PartialEq, Eq)]
824pub struct ReplicationPacket {
825 pub wire_version: ReplicationWireVersion,
827 pub changeset_id: ChangesetId,
829 pub sbn: u8,
831 pub esi: u32,
833 pub k_source: u32,
835 pub r_repair: u32,
837 pub symbol_size_t: u16,
839 pub seed: u64,
841 pub payload_xxh3: u64,
843 pub auth_tag: Option<[u8; 16]>,
845 pub symbol_data: Vec<u8>,
847}
848
849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
851pub enum ReplicationWireVersion {
852 LegacyV1,
854 FramedV2,
856}
857
858#[derive(Debug, Clone, Copy, PartialEq, Eq)]
860pub struct ReplicationPacketV2Header {
861 pub changeset_id: ChangesetId,
862 pub sbn: u8,
863 pub esi: u32,
864 pub k_source: u32,
865 pub r_repair: u32,
866 pub symbol_size_t: u16,
867 pub seed: u64,
868}
869
870impl ReplicationPacket {
871 #[must_use]
873 pub fn new_v2(header: ReplicationPacketV2Header, symbol_data: Vec<u8>) -> Self {
874 let payload_xxh3 = Self::compute_payload_xxh3(&symbol_data);
875 Self {
876 wire_version: ReplicationWireVersion::FramedV2,
877 changeset_id: header.changeset_id,
878 sbn: header.sbn,
879 esi: header.esi,
880 k_source: header.k_source,
881 r_repair: header.r_repair,
882 symbol_size_t: header.symbol_size_t,
883 seed: header.seed,
884 payload_xxh3,
885 auth_tag: None,
886 symbol_data,
887 }
888 }
889
890 #[must_use]
892 pub fn compute_payload_xxh3(symbol_data: &[u8]) -> u64 {
893 xxhash_rust::xxh3::xxh3_64(symbol_data)
894 }
895
896 fn auth_material(&self) -> Vec<u8> {
897 let mut material = Vec::with_capacity(16 + 1 + 4 + 4 + 4 + 2 + 8 + 8);
898 material.extend_from_slice(self.changeset_id.as_bytes());
899 material.push(self.sbn);
900 material.extend_from_slice(&self.esi.to_be_bytes());
901 material.extend_from_slice(&self.k_source.to_be_bytes());
902 material.extend_from_slice(&self.r_repair.to_be_bytes());
903 material.extend_from_slice(&self.symbol_size_t.to_be_bytes());
904 material.extend_from_slice(&self.seed.to_be_bytes());
905 material.extend_from_slice(&self.payload_xxh3.to_be_bytes());
906 material
907 }
908
909 fn compute_auth_tag(&self, auth_key: &[u8; 32]) -> [u8; 16] {
910 let mut hasher = blake3::Hasher::new_keyed(auth_key);
911 hasher.update(REPLICATION_PACKET_AUTH_DOMAIN.as_bytes());
912 hasher.update(&self.auth_material());
913 hasher.update(&self.symbol_data);
917 let digest = hasher.finalize();
918 let mut out = [0_u8; 16];
919 out.copy_from_slice(&digest.as_bytes()[..16]);
920 out
921 }
922
923 pub fn attach_auth_tag(&mut self, auth_key: &[u8; 32]) {
925 self.auth_tag = Some(self.compute_auth_tag(auth_key));
926 }
927
928 #[must_use]
930 pub fn verify_integrity(&self, auth_key: Option<&[u8; 32]>) -> bool {
931 if Self::compute_payload_xxh3(&self.symbol_data) != self.payload_xxh3 {
932 return false;
933 }
934 match (self.auth_tag, auth_key) {
935 (Some(tag), Some(key)) => auth_tags_equal(&tag, &self.compute_auth_tag(key)),
936 (Some(_), None) | (None, Some(_)) => false,
937 (None, None) => true,
938 }
939 }
940
941 pub fn validate_symbol_size(symbol_size: usize) -> Result<()> {
947 if symbol_size > MAX_REPLICATION_SYMBOL_SIZE {
948 error!(
949 bead_id = BEAD_ID,
950 symbol_size,
951 max = MAX_REPLICATION_SYMBOL_SIZE,
952 "symbol size exceeds UDP hard wire limit"
953 );
954 return Err(FrankenError::OutOfRange {
955 what: "symbol_size".to_owned(),
956 value: symbol_size.to_string(),
957 });
958 }
959 Ok(())
960 }
961
962 pub fn to_bytes(&self) -> Result<Vec<u8>> {
968 if self.esi > 0x00FF_FFFF {
969 return Err(FrankenError::OutOfRange {
970 what: "esi".to_owned(),
971 value: self.esi.to_string(),
972 });
973 }
974 if usize::from(self.symbol_size_t) != self.symbol_data.len() {
975 return Err(FrankenError::DatabaseCorrupt {
976 detail: format!(
977 "symbol_size_t mismatch: header={}, payload={}",
978 self.symbol_size_t,
979 self.symbol_data.len()
980 ),
981 });
982 }
983 let computed_xxh3 = Self::compute_payload_xxh3(&self.symbol_data);
984 if computed_xxh3 != self.payload_xxh3 {
985 return Err(FrankenError::DatabaseCorrupt {
986 detail: format!(
987 "payload_xxh3 mismatch before encoding: header={:#x}, payload={:#x}",
988 self.payload_xxh3, computed_xxh3
989 ),
990 });
991 }
992 Self::validate_symbol_size(self.symbol_data.len())?;
993
994 match self.wire_version {
995 ReplicationWireVersion::LegacyV1 => {
996 let total = REPLICATION_HEADER_SIZE_LEGACY + self.symbol_data.len();
997 let mut buf = Vec::with_capacity(total);
998 buf.extend_from_slice(self.changeset_id.as_bytes());
999 buf.push(self.sbn);
1000 let esi_bytes = self.esi.to_be_bytes();
1001 buf.extend_from_slice(&esi_bytes[1..4]);
1002 buf.extend_from_slice(&self.k_source.to_be_bytes());
1003 buf.extend_from_slice(&self.symbol_data);
1004 Ok(buf)
1005 }
1006 ReplicationWireVersion::FramedV2 => {
1007 let total = REPLICATION_HEADER_SIZE + self.symbol_data.len();
1008 let mut buf = Vec::with_capacity(total);
1009 let mut flags = 0_u8;
1010 if self.auth_tag.is_some() {
1011 flags |= REPLICATION_FLAG_AUTH_PRESENT;
1012 }
1013 buf.extend_from_slice(&REPLICATION_PROTOCOL_MAGIC);
1014 buf.push(REPLICATION_PROTOCOL_VERSION_V2);
1015 buf.push(flags);
1016 buf.extend_from_slice(&REPLICATION_HEADER_SIZE_V2_U16.to_be_bytes());
1017 buf.extend_from_slice(self.changeset_id.as_bytes());
1018 buf.push(self.sbn);
1019 let esi_bytes = self.esi.to_be_bytes();
1020 buf.extend_from_slice(&esi_bytes[1..4]);
1021 buf.extend_from_slice(&self.k_source.to_be_bytes());
1022 buf.extend_from_slice(&self.r_repair.to_be_bytes());
1023 buf.extend_from_slice(&self.symbol_size_t.to_be_bytes());
1024 buf.extend_from_slice(&0_u16.to_be_bytes()); buf.extend_from_slice(&self.seed.to_be_bytes());
1026 buf.extend_from_slice(&self.payload_xxh3.to_be_bytes());
1027 if let Some(tag) = self.auth_tag {
1028 buf.extend_from_slice(&tag);
1029 } else {
1030 buf.extend_from_slice(&[0_u8; 16]);
1031 }
1032 buf.extend_from_slice(&self.symbol_data);
1033 Ok(buf)
1034 }
1035 }
1036 }
1037
1038 pub fn from_bytes(buf: &[u8]) -> Result<Self> {
1044 if buf.len() < REPLICATION_HEADER_SIZE_LEGACY {
1045 return Err(FrankenError::DatabaseCorrupt {
1046 detail: format!(
1047 "replication packet too short: {} < {REPLICATION_HEADER_SIZE_LEGACY}",
1048 buf.len()
1049 ),
1050 });
1051 }
1052 let is_v2 = buf.len() >= REPLICATION_HEADER_SIZE
1053 && buf[0..4] == REPLICATION_PROTOCOL_MAGIC
1054 && buf[4] == REPLICATION_PROTOCOL_VERSION_V2;
1055 if is_v2 {
1056 let flags = buf[5];
1057 let unsupported_flags = flags & !REPLICATION_FLAG_AUTH_PRESENT;
1058 if unsupported_flags != 0 {
1059 return Err(FrankenError::DatabaseCorrupt {
1060 detail: format!(
1061 "unsupported replication packet flags: {unsupported_flags:#04x}"
1062 ),
1063 });
1064 }
1065 let header_len = usize::from(u16::from_be_bytes([buf[6], buf[7]]));
1066 if header_len != REPLICATION_HEADER_SIZE {
1067 return Err(FrankenError::DatabaseCorrupt {
1068 detail: format!(
1069 "unsupported replication header length: expected {}, got {header_len}",
1070 REPLICATION_HEADER_SIZE
1071 ),
1072 });
1073 }
1074 if buf.len() < header_len {
1075 return Err(FrankenError::DatabaseCorrupt {
1076 detail: format!("packet shorter than declared header length: {header_len}"),
1077 });
1078 }
1079 let mut id_bytes = [0_u8; 16];
1080 id_bytes.copy_from_slice(&buf[8..24]);
1081 let changeset_id = ChangesetId::from_bytes(id_bytes);
1082 let sbn = buf[24];
1083 let esi = u32::from(buf[25]) << 16 | u32::from(buf[26]) << 8 | u32::from(buf[27]);
1084 let k_source = u32::from_be_bytes(buf[28..32].try_into().expect("4 bytes"));
1085 let r_repair = u32::from_be_bytes(buf[32..36].try_into().expect("4 bytes"));
1086 let symbol_size_t = u16::from_be_bytes(buf[36..38].try_into().expect("2 bytes"));
1087 if buf[38] != 0 || buf[39] != 0 {
1088 return Err(FrankenError::DatabaseCorrupt {
1089 detail: "replication packet reserved bytes must be zero".to_owned(),
1090 });
1091 }
1092 let seed = u64::from_be_bytes(buf[40..48].try_into().expect("8 bytes"));
1093 let payload_xxh3 = u64::from_be_bytes(buf[48..56].try_into().expect("8 bytes"));
1094 let mut auth_tag_bytes = [0_u8; 16];
1095 auth_tag_bytes.copy_from_slice(&buf[56..72]);
1096 let auth_tag = if (flags & REPLICATION_FLAG_AUTH_PRESENT) != 0 {
1097 Some(auth_tag_bytes)
1098 } else {
1099 None
1100 };
1101 let symbol_data = buf[header_len..].to_vec();
1102 if symbol_data.len() != usize::from(symbol_size_t) {
1103 return Err(FrankenError::DatabaseCorrupt {
1104 detail: format!(
1105 "symbol_size_t mismatch in packet: header={symbol_size_t}, payload={}",
1106 symbol_data.len()
1107 ),
1108 });
1109 }
1110 return Ok(Self {
1111 wire_version: ReplicationWireVersion::FramedV2,
1112 changeset_id,
1113 sbn,
1114 esi,
1115 k_source,
1116 r_repair,
1117 symbol_size_t,
1118 seed,
1119 payload_xxh3,
1120 auth_tag,
1121 symbol_data,
1122 });
1123 }
1124
1125 let mut id_bytes = [0_u8; 16];
1126 id_bytes.copy_from_slice(&buf[0..16]);
1127 let changeset_id = ChangesetId::from_bytes(id_bytes);
1128 let sbn = buf[16];
1129 let esi = u32::from(buf[17]) << 16 | u32::from(buf[18]) << 8 | u32::from(buf[19]);
1130 let k_source = u32::from_be_bytes(buf[20..24].try_into().expect("4 bytes"));
1131 let symbol_data = buf[24..].to_vec();
1132 let symbol_size_t =
1133 u16::try_from(symbol_data.len()).map_err(|_| FrankenError::OutOfRange {
1134 what: "symbol_size_t".to_owned(),
1135 value: symbol_data.len().to_string(),
1136 })?;
1137
1138 Ok(Self {
1139 wire_version: ReplicationWireVersion::LegacyV1,
1140 changeset_id,
1141 sbn,
1142 esi,
1143 k_source,
1144 r_repair: 0,
1145 symbol_size_t,
1146 seed: derive_seed_from_changeset_id(&changeset_id),
1147 payload_xxh3: Self::compute_payload_xxh3(&symbol_data),
1148 auth_tag: None,
1149 symbol_data,
1150 })
1151 }
1152
1153 #[must_use]
1155 pub fn wire_size(&self) -> usize {
1156 let header_size = match self.wire_version {
1157 ReplicationWireVersion::LegacyV1 => REPLICATION_HEADER_SIZE_LEGACY,
1158 ReplicationWireVersion::FramedV2 => REPLICATION_HEADER_SIZE,
1159 };
1160 header_size + self.symbol_data.len()
1161 }
1162
1163 #[must_use]
1165 pub fn is_source_symbol(&self) -> bool {
1166 self.esi < self.k_source
1167 }
1168}
1169
1170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1176pub enum SenderState {
1177 Idle,
1179 Encoding,
1181 Streaming,
1183 Complete,
1185}
1186
1187#[derive(Debug, Clone)]
1189pub struct SenderConfig {
1190 pub symbol_size: u16,
1192 pub max_isi_multiplier: u32,
1194}
1195
1196impl Default for SenderConfig {
1197 fn default() -> Self {
1198 Self {
1199 symbol_size: MTU_SAFE_SYMBOL_SIZE,
1200 max_isi_multiplier: DEFAULT_MAX_ISI_MULTIPLIER,
1201 }
1202 }
1203}
1204
1205#[derive(Debug)]
1207pub struct EncodingSession {
1208 pub shards: Vec<ChangesetShard>,
1210 pub current_shard: usize,
1212 pub current_isi: u32,
1214 pub config: SenderConfig,
1216}
1217
1218#[derive(Debug)]
1220pub struct ReplicationSender {
1221 state: SenderState,
1222 session: Option<EncodingSession>,
1223}
1224
1225impl ReplicationSender {
1226 #[must_use]
1228 pub fn new() -> Self {
1229 Self {
1230 state: SenderState::Idle,
1231 session: None,
1232 }
1233 }
1234
1235 #[must_use]
1237 pub const fn state(&self) -> SenderState {
1238 self.state
1239 }
1240
1241 pub fn prepare(
1247 &mut self,
1248 page_size: u32,
1249 pages: &mut [PageEntry],
1250 config: SenderConfig,
1251 ) -> Result<()> {
1252 if self.state != SenderState::Idle {
1253 return Err(FrankenError::Internal(format!(
1254 "sender must be IDLE to prepare, current state: {:?}",
1255 self.state
1256 )));
1257 }
1258
1259 ReplicationPacket::validate_symbol_size(usize::from(config.symbol_size))?;
1260
1261 let changeset_bytes = encode_changeset(page_size, pages)?;
1262 let shards = shard_changeset(changeset_bytes, config.symbol_size)?;
1263
1264 info!(
1265 bead_id = BEAD_ID,
1266 n_shards = shards.len(),
1267 symbol_size = config.symbol_size,
1268 "sender prepared for streaming"
1269 );
1270
1271 self.session = Some(EncodingSession {
1272 shards,
1273 current_shard: 0,
1274 current_isi: 0,
1275 config,
1276 });
1277 self.state = SenderState::Encoding;
1278 Ok(())
1279 }
1280
1281 pub fn start_streaming(&mut self) -> Result<()> {
1287 if self.state != SenderState::Encoding {
1288 return Err(FrankenError::Internal(format!(
1289 "sender must be ENCODING to start streaming, current state: {:?}",
1290 self.state
1291 )));
1292 }
1293 self.state = SenderState::Streaming;
1294 info!(bead_id = BEAD_ID, "sender started streaming");
1295 Ok(())
1296 }
1297
1298 #[allow(clippy::too_many_lines)]
1306 pub fn next_packet(&mut self) -> Result<Option<ReplicationPacket>> {
1307 if self.state != SenderState::Streaming {
1308 return Err(FrankenError::Internal(format!(
1309 "sender must be STREAMING to generate packets, current state: {:?}",
1310 self.state
1311 )));
1312 }
1313
1314 let session = self
1315 .session
1316 .as_mut()
1317 .expect("session exists in STREAMING state");
1318
1319 if session.current_shard >= session.shards.len() {
1320 return Ok(None);
1322 }
1323
1324 let shard = &session.shards[session.current_shard];
1325 let max_isi = shard
1326 .k_source
1327 .saturating_mul(session.config.max_isi_multiplier);
1328
1329 if session.current_isi >= max_isi {
1330 session.current_shard += 1;
1332 session.current_isi = 0;
1333
1334 if session.current_shard >= session.shards.len() {
1335 return Ok(None);
1336 }
1337
1338 let next_shard = &session.shards[session.current_shard];
1339 debug!(
1340 bead_id = BEAD_ID,
1341 shard_index = session.current_shard,
1342 k_source = next_shard.k_source,
1343 "advancing to next shard"
1344 );
1345 }
1346
1347 let shard = &session.shards[session.current_shard];
1348 let isi = session.current_isi;
1349 let t = usize::from(session.config.symbol_size);
1350
1351 let symbol_data = if u64::from(isi) < u64::from(shard.k_source) {
1356 let start = isi as usize * t;
1358 let end = (start + t).min(shard.changeset_bytes.len());
1359 let mut data = vec![0_u8; t];
1360 let available = end.saturating_sub(start);
1361 if available > 0 {
1362 data[..available].copy_from_slice(&shard.changeset_bytes[start..end]);
1363 }
1364 data
1366 } else {
1367 #[cfg(not(target_arch = "wasm32"))]
1368 {
1369 use asupersync::raptorq::systematic::SystematicEncoder;
1378
1379 let source_symbols: Vec<Vec<u8>> = (0..shard.k_source as usize)
1380 .map(|i| {
1381 let start = i * t;
1382 let end = (start + t).min(shard.changeset_bytes.len());
1383 let mut sym = vec![0_u8; t];
1384 let available = end.saturating_sub(start);
1385 if available > 0 {
1386 sym[..available].copy_from_slice(&shard.changeset_bytes[start..end]);
1387 }
1388 sym
1389 })
1390 .collect();
1391
1392 match SystematicEncoder::new(&source_symbols, t, shard.seed) {
1393 Some(encoder) => encoder.repair_symbol(isi),
1394 None => {
1395 warn!(
1396 bead_id = BEAD_ID,
1397 isi,
1398 shard_index = session.current_shard,
1399 "RaptorQ encoder construction failed; using placeholder repair symbol"
1400 );
1401 generate_deterministic_placeholder(shard.seed, isi, t)
1402 }
1403 }
1404 }
1405 #[cfg(target_arch = "wasm32")]
1406 {
1407 warn!(
1408 bead_id = BEAD_ID,
1409 isi,
1410 shard_index = session.current_shard,
1411 "RaptorQ encoder is native-only; using placeholder repair symbol"
1412 );
1413 generate_deterministic_placeholder(shard.seed, isi, t)
1414 }
1415 };
1416
1417 let r_repair = max_isi.saturating_sub(shard.k_source);
1418 let packet = ReplicationPacket::new_v2(
1419 ReplicationPacketV2Header {
1420 changeset_id: shard.changeset_id,
1421 sbn: 0, esi: isi,
1423 k_source: shard.k_source,
1424 r_repair,
1425 symbol_size_t: session.config.symbol_size,
1426 seed: shard.seed,
1427 },
1428 symbol_data,
1429 );
1430
1431 session.current_isi += 1;
1432 Ok(Some(packet))
1433 }
1434
1435 pub fn acknowledge_complete(&mut self) -> Result<()> {
1441 if self.state != SenderState::Streaming {
1442 return Err(FrankenError::Internal(format!(
1443 "sender must be STREAMING to acknowledge, current state: {:?}",
1444 self.state
1445 )));
1446 }
1447 self.state = SenderState::Complete;
1448 info!(bead_id = BEAD_ID, "sender acknowledged completion");
1449 Ok(())
1450 }
1451
1452 pub fn complete(&mut self) {
1456 if self.state == SenderState::Streaming || self.state == SenderState::Encoding {
1457 self.state = SenderState::Complete;
1458 info!(bead_id = BEAD_ID, "sender completed");
1459 }
1460 }
1461
1462 pub fn reset(&mut self) {
1464 self.state = SenderState::Idle;
1465 self.session = None;
1466 debug!(bead_id = BEAD_ID, "sender reset to IDLE");
1467 }
1468}
1469
1470impl Default for ReplicationSender {
1471 fn default() -> Self {
1472 Self::new()
1473 }
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478 use super::*;
1479
1480 const TEST_BEAD_ID: &str = "bd-1hi.13";
1481 const TEST_BEAD_BD_1SQU: &str = "bd-1squ";
1482
1483 #[allow(clippy::cast_possible_truncation)]
1484 fn make_pages(page_size: u32, page_numbers: &[u32]) -> Vec<PageEntry> {
1485 page_numbers
1486 .iter()
1487 .map(|&pn| {
1488 let mut data = vec![0_u8; page_size as usize];
1489 for (i, byte) in data.iter_mut().enumerate() {
1491 *byte = ((pn as usize * 251 + i * 31) % 256) as u8;
1492 }
1493 PageEntry::new(pn, data)
1494 })
1495 .collect()
1496 }
1497
1498 #[test]
1503 fn test_changeset_header_format() {
1504 let header = ChangesetHeader {
1505 magic: CHANGESET_MAGIC,
1506 version: CHANGESET_VERSION,
1507 page_size: 4096,
1508 n_pages: 10,
1509 total_len: 42_000,
1510 };
1511 let bytes = header.to_bytes();
1512 assert_eq!(
1513 &bytes[0..4],
1514 b"FSRP",
1515 "bead_id={TEST_BEAD_ID} case=header_magic"
1516 );
1517 assert_eq!(bytes.len(), CHANGESET_HEADER_SIZE);
1518
1519 let decoded = ChangesetHeader::from_bytes(&bytes).expect("decode should succeed");
1520 assert_eq!(
1521 header, decoded,
1522 "bead_id={TEST_BEAD_ID} case=header_roundtrip"
1523 );
1524 }
1525
1526 #[test]
1527 fn test_changeset_encoding_deterministic() {
1528 let page_size = 512_u32;
1529 let mut pages_a = make_pages(page_size, &[3, 1, 2]);
1530 let mut pages_b = make_pages(page_size, &[2, 3, 1]); let bytes_a = encode_changeset(page_size, &mut pages_a).expect("encode a");
1533 let bytes_b = encode_changeset(page_size, &mut pages_b).expect("encode b");
1534
1535 assert_eq!(
1537 bytes_a, bytes_b,
1538 "bead_id={TEST_BEAD_ID} case=deterministic_encoding"
1539 );
1540
1541 let id_a = compute_changeset_id(&bytes_a);
1543 let id_b = compute_changeset_id(&bytes_b);
1544 assert_eq!(
1545 id_a, id_b,
1546 "bead_id={TEST_BEAD_ID} case=deterministic_changeset_id"
1547 );
1548 }
1549
1550 #[test]
1551 fn test_changeset_id_domain_separation() {
1552 let data = b"test payload";
1553
1554 let changeset_id = compute_changeset_id(data);
1556
1557 let mut hasher = blake3::Hasher::new();
1559 hasher.update(b"fsqlite:ecs:v1");
1560 hasher.update(data);
1561 let ecs_hash = hasher.finalize();
1562 let mut ecs_id = [0_u8; 16];
1563 ecs_id.copy_from_slice(&ecs_hash.as_bytes()[..16]);
1564
1565 assert_ne!(
1566 changeset_id.as_bytes(),
1567 &ecs_id,
1568 "bead_id={TEST_BEAD_ID} case=domain_separation"
1569 );
1570 }
1571
1572 #[test]
1573 fn test_seed_derivation() {
1574 let id = ChangesetId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
1575 let seed = derive_seed_from_changeset_id(&id);
1576
1577 let seed2 = derive_seed_from_changeset_id(&id);
1579 assert_eq!(
1580 seed, seed2,
1581 "bead_id={TEST_BEAD_ID} case=seed_deterministic"
1582 );
1583
1584 assert_ne!(seed, 0, "bead_id={TEST_BEAD_ID} case=seed_nonzero");
1586 }
1587
1588 #[test]
1589 fn test_bd_1squ_changeset_id_stability() {
1590 let payload = b"deterministic-changeset-payload";
1591 let id_a = compute_changeset_id(payload);
1592 let id_b = compute_changeset_id(payload);
1593 assert_eq!(
1594 id_a, id_b,
1595 "bead_id={TEST_BEAD_BD_1SQU} case=id_stability_same_payload"
1596 );
1597
1598 let mut altered = payload.to_vec();
1599 altered[0] ^= 0xFF;
1600 let id_c = compute_changeset_id(&altered);
1601 assert_ne!(
1602 id_a, id_c,
1603 "bead_id={TEST_BEAD_BD_1SQU} case=id_stability_diff_payload"
1604 );
1605 }
1606
1607 #[test]
1608 fn test_bd_1squ_seed_stability() {
1609 let id = compute_changeset_id(b"seed-stability");
1610 let seed_a = derive_seed_from_changeset_id(&id);
1611 let seed_b = derive_seed_from_changeset_id(&id);
1612 assert_eq!(
1613 seed_a, seed_b,
1614 "bead_id={TEST_BEAD_BD_1SQU} case=seed_stability_same_id"
1615 );
1616
1617 let other = compute_changeset_id(b"seed-stability-other");
1618 let seed_other = derive_seed_from_changeset_id(&other);
1619 assert_ne!(
1620 seed_a, seed_other,
1621 "bead_id={TEST_BEAD_BD_1SQU} case=seed_stability_diff_id"
1622 );
1623 }
1624
1625 #[test]
1626 fn test_bd_1squ_k_source_computation() {
1627 assert_eq!(
1628 compute_k_source(0, 256).expect("k_source"),
1629 0,
1630 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_empty"
1631 );
1632 assert_eq!(
1633 compute_k_source(1, 256).expect("k_source"),
1634 1,
1635 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_single_byte"
1636 );
1637 assert_eq!(
1638 compute_k_source(256, 256).expect("k_source"),
1639 1,
1640 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_exact_division"
1641 );
1642 assert_eq!(
1643 compute_k_source(257, 256).expect("k_source"),
1644 2,
1645 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_round_up"
1646 );
1647 assert_eq!(
1648 compute_k_source(usize::try_from(K_MAX).unwrap() * 64, 64).expect("k_source"),
1649 u64::from(K_MAX),
1650 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_kmax_boundary"
1651 );
1652 assert_eq!(
1653 compute_k_source(usize::try_from(K_MAX).unwrap() * 64 + 1, 64).expect("k_source"),
1654 u64::from(K_MAX) + 1,
1655 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_kmax_plus_one"
1656 );
1657 assert!(
1658 compute_k_source(10, 0).is_err(),
1659 "bead_id={TEST_BEAD_BD_1SQU} case=k_source_zero_symbol_rejected"
1660 );
1661 }
1662
1663 #[test]
1664 fn test_bd_1squ_sharding_threshold_rule() {
1665 let symbol_size = 64_u16;
1666 let max_payload = usize::try_from(u64::from(K_MAX) * u64::from(symbol_size)).unwrap();
1667
1668 let exact = vec![0xA5_u8; max_payload];
1669 let exact_shards = shard_changeset(exact, symbol_size).expect("exact shard");
1670 assert_eq!(
1671 exact_shards.len(),
1672 1,
1673 "bead_id={TEST_BEAD_BD_1SQU} case=exact_threshold_single_shard"
1674 );
1675 assert_eq!(
1676 exact_shards[0].k_source, K_MAX,
1677 "bead_id={TEST_BEAD_BD_1SQU} case=exact_threshold_kmax"
1678 );
1679
1680 let over = vec![0x5A_u8; max_payload + 1];
1681 let over_shards = shard_changeset(over, symbol_size).expect("over shard");
1682 assert_eq!(
1683 over_shards.len(),
1684 2,
1685 "bead_id={TEST_BEAD_BD_1SQU} case=over_threshold_two_shards"
1686 );
1687 assert_eq!(
1688 over_shards[0].k_source, K_MAX,
1689 "bead_id={TEST_BEAD_BD_1SQU} case=over_threshold_first_kmax"
1690 );
1691 assert_eq!(
1692 over_shards[1].k_source, 1,
1693 "bead_id={TEST_BEAD_BD_1SQU} case=over_threshold_second_one_symbol"
1694 );
1695 }
1696
1697 #[test]
1698 fn test_page_entries_sorted() {
1699 let page_size = 128_u32;
1700 let mut pages = make_pages(page_size, &[5, 1, 3, 2, 4]);
1701 let bytes = encode_changeset(page_size, &mut pages).expect("encode");
1702
1703 assert_eq!(pages[0].page_number, 1);
1705 assert_eq!(pages[1].page_number, 2);
1706 assert_eq!(pages[2].page_number, 3);
1707 assert_eq!(pages[3].page_number, 4);
1708 assert_eq!(pages[4].page_number, 5);
1709
1710 let header_bytes: [u8; CHANGESET_HEADER_SIZE] =
1712 bytes[..CHANGESET_HEADER_SIZE].try_into().unwrap();
1713 let header = ChangesetHeader::from_bytes(&header_bytes).expect("decode header");
1714 assert_eq!(
1715 header.total_len,
1716 bytes.len() as u64,
1717 "bead_id={TEST_BEAD_ID} case=total_len_matches"
1718 );
1719 assert_eq!(header.n_pages, 5);
1720 }
1721
1722 #[test]
1723 fn test_page_xxh3_validation() {
1724 let page = PageEntry::new(1, vec![0xAA; 4096]);
1725 assert!(
1726 page.validate_xxh3(),
1727 "bead_id={TEST_BEAD_ID} case=xxh3_valid"
1728 );
1729
1730 let mut tampered = page;
1732 tampered.page_bytes[0] ^= 0xFF;
1733 assert!(
1734 !tampered.validate_xxh3(),
1735 "bead_id={TEST_BEAD_ID} case=xxh3_tampered"
1736 );
1737 }
1738
1739 #[test]
1740 fn test_encode_changeset_rejects_page_size_mismatch() {
1741 let mut pages = vec![PageEntry::new(1, vec![0xAA; 127])];
1742 let result = encode_changeset(128, &mut pages);
1743 assert!(
1744 matches!(result, Err(FrankenError::OutOfRange { .. })),
1745 "bead_id={TEST_BEAD_ID} case=page_size_mismatch_rejected"
1746 );
1747 }
1748
1749 #[test]
1750 fn test_encode_changeset_rejects_stale_page_checksum() {
1751 let page_size = 128_u32;
1752 let mut pages = make_pages(page_size, &[1]);
1753 pages[0].page_bytes[0] ^= 0xFF;
1754
1755 let result = encode_changeset(page_size, &mut pages);
1756 assert!(
1757 matches!(result, Err(FrankenError::DatabaseCorrupt { .. })),
1758 "bead_id={TEST_BEAD_ID} case=stale_page_checksum_rejected"
1759 );
1760 }
1761
1762 #[test]
1767 fn test_udp_packet_format() {
1768 let id = ChangesetId::from_bytes([0xAA; 16]);
1769 let packet = ReplicationPacket::new_v2(
1770 ReplicationPacketV2Header {
1771 changeset_id: id,
1772 sbn: 0,
1773 esi: 42,
1774 k_source: 100,
1775 r_repair: 12,
1776 symbol_size_t: 512,
1777 seed: derive_seed_from_changeset_id(&id),
1778 },
1779 vec![0x55; 512],
1780 );
1781
1782 let wire = packet.to_bytes().expect("encode");
1783 assert_eq!(
1784 wire.len(),
1785 REPLICATION_HEADER_SIZE + 512,
1786 "bead_id={TEST_BEAD_ID} case=packet_size"
1787 );
1788
1789 assert_eq!(&wire[0..4], &REPLICATION_PROTOCOL_MAGIC);
1791 assert_eq!(wire[4], REPLICATION_PROTOCOL_VERSION_V2);
1792 assert_eq!(wire[5], 0, "flags");
1793 assert_eq!(&wire[8..24], &[0xAA; 16], "changeset_id");
1794 assert_eq!(wire[24], 0, "sbn");
1795 assert_eq!(&wire[25..28], &[0, 0, 42], "esi u24 big-endian");
1796 assert_eq!(&wire[28..32], &100_u32.to_be_bytes(), "k_source");
1797 assert_eq!(&wire[32..36], &12_u32.to_be_bytes(), "r_repair");
1798 assert_eq!(&wire[36..38], &512_u16.to_be_bytes(), "symbol_size_t");
1799
1800 let decoded = ReplicationPacket::from_bytes(&wire).expect("decode");
1802 assert_eq!(
1803 packet, decoded,
1804 "bead_id={TEST_BEAD_ID} case=packet_roundtrip"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_v2_packet_rejects_unknown_flags_and_reserved_bytes() {
1810 let id = ChangesetId::from_bytes([0xAB; 16]);
1811 let packet = ReplicationPacket::new_v2(
1812 ReplicationPacketV2Header {
1813 changeset_id: id,
1814 sbn: 0,
1815 esi: 7,
1816 k_source: 9,
1817 r_repair: 1,
1818 symbol_size_t: 16,
1819 seed: derive_seed_from_changeset_id(&id),
1820 },
1821 vec![0x11; 16],
1822 );
1823
1824 let mut unknown_flags = packet.to_bytes().expect("encode");
1825 unknown_flags[5] |= 0b1000_0000;
1826 assert!(
1827 matches!(
1828 ReplicationPacket::from_bytes(&unknown_flags),
1829 Err(FrankenError::DatabaseCorrupt { .. })
1830 ),
1831 "bead_id={TEST_BEAD_ID} case=unknown_flags_rejected"
1832 );
1833
1834 let mut nonzero_reserved = packet.to_bytes().expect("encode");
1835 nonzero_reserved[38] = 1;
1836 assert!(
1837 matches!(
1838 ReplicationPacket::from_bytes(&nonzero_reserved),
1839 Err(FrankenError::DatabaseCorrupt { .. })
1840 ),
1841 "bead_id={TEST_BEAD_ID} case=reserved_bytes_rejected"
1842 );
1843 }
1844
1845 #[test]
1846 fn test_auth_tag_covers_symbol_bytes_not_only_xxh3() {
1847 let key = [0x42; 32];
1848 let id = ChangesetId::from_bytes([0xCD; 16]);
1849 let packet = ReplicationPacket::new_v2(
1850 ReplicationPacketV2Header {
1851 changeset_id: id,
1852 sbn: 0,
1853 esi: 1,
1854 k_source: 2,
1855 r_repair: 0,
1856 symbol_size_t: 8,
1857 seed: derive_seed_from_changeset_id(&id),
1858 },
1859 vec![0x55; 8],
1860 );
1861 let mut altered = packet.clone();
1862 altered.symbol_data[0] ^= 0xFF;
1863 altered.payload_xxh3 = packet.payload_xxh3;
1864
1865 assert_ne!(
1866 packet.compute_auth_tag(&key),
1867 altered.compute_auth_tag(&key),
1868 "bead_id={TEST_BEAD_ID} case=auth_tag_covers_payload_bytes"
1869 );
1870 }
1871
1872 #[test]
1873 fn test_udp_packet_mtu_safe() {
1874 let t = usize::from(MTU_SAFE_SYMBOL_SIZE);
1876 let total = REPLICATION_HEADER_SIZE + t;
1877 assert_eq!(
1878 total, 1472,
1879 "bead_id={TEST_BEAD_ID} case=mtu_safe_packet_size"
1880 );
1881 assert_eq!(total + 20 + 8, 1500, "fits in Ethernet MTU");
1883 }
1884
1885 #[test]
1886 fn test_hard_wire_limit() {
1887 let oversized = MAX_REPLICATION_SYMBOL_SIZE + 1;
1889 let result = ReplicationPacket::validate_symbol_size(oversized);
1890 assert!(
1891 result.is_err(),
1892 "bead_id={TEST_BEAD_ID} case=hard_wire_limit_rejected"
1893 );
1894
1895 let at_limit = MAX_REPLICATION_SYMBOL_SIZE;
1897 let result = ReplicationPacket::validate_symbol_size(at_limit);
1898 assert!(
1899 result.is_ok(),
1900 "bead_id={TEST_BEAD_ID} case=hard_wire_limit_at_max"
1901 );
1902 }
1903
1904 #[test]
1909 fn test_sender_idle_to_encoding() {
1910 let mut sender = ReplicationSender::new();
1911 assert_eq!(sender.state(), SenderState::Idle);
1912
1913 let mut pages = make_pages(512, &[1, 2, 3]);
1914 sender
1915 .prepare(512, &mut pages, SenderConfig::default())
1916 .expect("prepare");
1917 assert_eq!(
1918 sender.state(),
1919 SenderState::Encoding,
1920 "bead_id={TEST_BEAD_ID} case=idle_to_encoding"
1921 );
1922 }
1923
1924 #[test]
1925 fn test_streaming_source_then_repair() {
1926 let mut sender = ReplicationSender::new();
1927 let mut pages = make_pages(512, &[1, 2]);
1928 let config = SenderConfig {
1929 symbol_size: 512,
1930 max_isi_multiplier: 2,
1931 };
1932 sender.prepare(512, &mut pages, config).expect("prepare");
1933 sender.start_streaming().expect("start");
1934
1935 let session = sender.session.as_ref().unwrap();
1936 let k_source = session.shards[0].k_source;
1937
1938 let mut source_count = 0_u32;
1939 let mut repair_count = 0_u32;
1940 let mut last_isi = 0_u32;
1941
1942 while let Some(packet) = sender.next_packet().expect("next") {
1943 if packet.is_source_symbol() {
1944 source_count += 1;
1945 } else {
1946 repair_count += 1;
1947 }
1948 last_isi = packet.esi;
1949 }
1950
1951 assert!(
1952 source_count > 0,
1953 "bead_id={TEST_BEAD_ID} case=has_source_symbols"
1954 );
1955 assert!(
1956 repair_count > 0,
1957 "bead_id={TEST_BEAD_ID} case=has_repair_symbols"
1958 );
1959 assert_eq!(
1960 source_count, k_source,
1961 "bead_id={TEST_BEAD_ID} case=source_count_matches_k"
1962 );
1963 assert_eq!(
1964 last_isi,
1965 k_source * 2 - 1,
1966 "bead_id={TEST_BEAD_ID} case=max_isi_reached"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_streaming_systematic_first_ordering() {
1972 let mut sender = ReplicationSender::new();
1973 let mut pages = make_pages(512, &[1, 2]);
1974 let config = SenderConfig {
1975 symbol_size: 512,
1976 max_isi_multiplier: 2,
1977 };
1978 sender.prepare(512, &mut pages, config).expect("prepare");
1979 sender.start_streaming().expect("start");
1980
1981 let session = sender.session.as_ref().expect("session");
1982 let k_source = session.shards[0].k_source;
1983 let k_source_usize = usize::try_from(k_source).expect("K_source fits usize");
1984
1985 let mut observed_esis = Vec::new();
1986 while let Some(packet) = sender.next_packet().expect("next") {
1987 observed_esis.push(packet.esi);
1988 }
1989
1990 assert!(
1991 observed_esis.len() >= k_source_usize,
1992 "bead_id={TEST_BEAD_ID} case=have_at_least_k_source_packets"
1993 );
1994
1995 let expected_systematic: Vec<u32> = (0..k_source).collect();
1996 assert_eq!(
1997 &observed_esis[..k_source_usize],
1998 expected_systematic.as_slice(),
1999 "bead_id={TEST_BEAD_ID} case=systematic_first_ordering"
2000 );
2001
2002 if observed_esis.len() > k_source_usize {
2003 assert!(
2004 observed_esis[k_source_usize] >= k_source,
2005 "bead_id={TEST_BEAD_ID} case=repair_starts_after_systematic"
2006 );
2007 }
2008 }
2009
2010 #[test]
2011 fn test_streaming_schedule_deterministic_across_runs() {
2012 fn collect_packets(
2013 page_size: u32,
2014 page_numbers: &[u32],
2015 config: &SenderConfig,
2016 ) -> Vec<ReplicationPacket> {
2017 let mut sender = ReplicationSender::new();
2018 let mut pages = make_pages(page_size, page_numbers);
2019 sender
2020 .prepare(page_size, &mut pages, config.clone())
2021 .expect("prepare");
2022 sender.start_streaming().expect("start");
2023
2024 let mut packets = Vec::new();
2025 while let Some(packet) = sender.next_packet().expect("next") {
2026 packets.push(packet);
2027 }
2028 packets
2029 }
2030
2031 let config = SenderConfig {
2032 symbol_size: 256,
2033 max_isi_multiplier: 2,
2034 };
2035 let run_a = collect_packets(512, &[1, 3, 2], &config);
2036 let run_b = collect_packets(512, &[1, 3, 2], &config);
2037
2038 assert_eq!(
2039 run_a.len(),
2040 run_b.len(),
2041 "bead_id={TEST_BEAD_ID} case=deterministic_run_packet_count"
2042 );
2043 assert_eq!(
2044 run_a, run_b,
2045 "bead_id={TEST_BEAD_ID} case=deterministic_schedule_reproducible"
2046 );
2047 }
2048
2049 #[test]
2050 fn test_streaming_stop_on_ack() {
2051 let mut sender = ReplicationSender::new();
2052 let mut pages = make_pages(512, &[1]);
2053 sender
2054 .prepare(512, &mut pages, SenderConfig::default())
2055 .expect("prepare");
2056 sender.start_streaming().expect("start");
2057
2058 let _p1 = sender.next_packet().expect("next").expect("packet");
2060
2061 sender.acknowledge_complete().expect("ack");
2063 assert_eq!(
2064 sender.state(),
2065 SenderState::Complete,
2066 "bead_id={TEST_BEAD_ID} case=stop_on_ack"
2067 );
2068 assert!(
2069 sender.next_packet().is_err(),
2070 "bead_id={TEST_BEAD_ID} case=no_packets_after_ack_complete"
2071 );
2072 }
2073
2074 #[test]
2075 fn test_streaming_stop_on_max_isi() {
2076 let mut sender = ReplicationSender::new();
2077 let mut pages = make_pages(128, &[1]);
2078 let config = SenderConfig {
2079 symbol_size: 128,
2080 max_isi_multiplier: 2,
2081 };
2082 sender.prepare(128, &mut pages, config).expect("prepare");
2083 sender.start_streaming().expect("start");
2084
2085 let mut count = 0_u32;
2086 while sender.next_packet().expect("next").is_some() {
2087 count += 1;
2088 }
2089
2090 let session = sender.session.as_ref().unwrap();
2092 let expected = session.shards[0].k_source * 2;
2093 assert_eq!(
2094 count, expected,
2095 "bead_id={TEST_BEAD_ID} case=stop_on_max_isi"
2096 );
2097 }
2098
2099 #[test]
2100 fn test_block_size_limit_sharding() {
2101 let symbol_size = 64_u16;
2103 let bytes_per_max_block = u64::from(K_MAX) * u64::from(symbol_size);
2104 let changeset_bytes = vec![0xAB_u8; usize::try_from(bytes_per_max_block).unwrap() + 1];
2106 let shards = shard_changeset(changeset_bytes.clone(), symbol_size).expect("shard");
2107
2108 assert!(
2109 shards.len() > 1,
2110 "bead_id={TEST_BEAD_ID} case=sharding_triggered shards={}",
2111 shards.len()
2112 );
2113
2114 for (i, shard) in shards.iter().enumerate() {
2116 assert!(
2117 shard.k_source <= K_MAX,
2118 "bead_id={TEST_BEAD_ID} case=shard_k_max shard={i} k_source={}",
2119 shard.k_source
2120 );
2121 }
2122
2123 let total_bytes: usize = shards.iter().map(|s| s.changeset_bytes.len()).sum();
2125 assert_eq!(
2126 total_bytes,
2127 changeset_bytes.len(),
2128 "bead_id={TEST_BEAD_ID} case=sharding_coverage"
2129 );
2130 }
2131
2132 #[test]
2137 fn prop_changeset_id_unique() {
2138 let page_size = 128_u32;
2139 let mut ids = Vec::new();
2140 for seed in 0_u32..20 {
2141 let mut pages = vec![PageEntry::new(
2142 1,
2143 vec![u8::try_from(seed).unwrap(); page_size as usize],
2144 )];
2145 let bytes = encode_changeset(page_size, &mut pages).expect("encode");
2146 ids.push(compute_changeset_id(&bytes));
2147 }
2148
2149 for i in 0..ids.len() {
2151 for j in (i + 1)..ids.len() {
2152 assert_ne!(
2153 ids[i], ids[j],
2154 "bead_id={TEST_BEAD_ID} case=prop_id_unique i={i} j={j}"
2155 );
2156 }
2157 }
2158 }
2159
2160 #[test]
2161 fn prop_sharding_covers_all_pages() {
2162 let symbol_size = 64_u16;
2163 for size_multiplier in [1_u64, 2, 5] {
2164 let total = u64::from(K_MAX) * u64::from(symbol_size) * size_multiplier + 7;
2165 let changeset = vec![0xCC_u8; usize::try_from(total).unwrap()];
2166 let shards = shard_changeset(changeset.clone(), symbol_size).expect("shard");
2167
2168 let reassembled: Vec<u8> = shards
2169 .iter()
2170 .flat_map(|s| s.changeset_bytes.iter().copied())
2171 .collect();
2172
2173 assert_eq!(
2174 reassembled, changeset,
2175 "bead_id={TEST_BEAD_ID} case=prop_sharding_coverage multiplier={size_multiplier}"
2176 );
2177 }
2178 }
2179
2180 #[test]
2185 fn test_bd_1hi_13_unit_compliance_gate() {
2186 assert_eq!(CHANGESET_MAGIC, *b"FSRP");
2187 assert_eq!(CHANGESET_VERSION, 1);
2188 assert_eq!(CHANGESET_HEADER_SIZE, 22);
2189 assert_eq!(REPLICATION_HEADER_SIZE_LEGACY, 24);
2190 assert_eq!(REPLICATION_HEADER_SIZE, 72);
2191 assert_eq!(REPLICATION_HEADER_SIZE_V2, 72);
2192 assert_eq!(MAX_UDP_PAYLOAD, 65_507);
2193 const { assert!(MAX_REPLICATION_SYMBOL_SIZE < MAX_UDP_PAYLOAD) };
2194
2195 let _ = ChangesetId::from_bytes([0; 16]);
2197 let _ = compute_changeset_id(b"test");
2198 let _ = derive_seed_from_changeset_id(&ChangesetId::from_bytes([0; 16]));
2199 }
2200
2201 #[test]
2202 fn prop_bd_1hi_13_structure_compliance() {
2203 let mut sender = ReplicationSender::new();
2205 assert_eq!(sender.state(), SenderState::Idle);
2206
2207 let mut pages = make_pages(256, &[1, 2]);
2208 sender
2209 .prepare(256, &mut pages, SenderConfig::default())
2210 .expect("prepare");
2211 assert_eq!(sender.state(), SenderState::Encoding);
2212
2213 sender.start_streaming().expect("start");
2214 assert_eq!(sender.state(), SenderState::Streaming);
2215
2216 sender.complete();
2217 assert_eq!(sender.state(), SenderState::Complete);
2218
2219 sender.reset();
2220 assert_eq!(sender.state(), SenderState::Idle);
2221 }
2222
2223 #[test]
2228 fn test_tls_by_default() {
2229 let cfg = NetworkStackConfig::default();
2230 assert_eq!(cfg.security, TransportSecurityMode::RustlsTls);
2231 assert!(cfg.validate_security().is_ok());
2232 }
2233
2234 #[test]
2235 fn test_plaintext_requires_explicit_opt_in() {
2236 let cfg = NetworkStackConfig {
2237 security: TransportSecurityMode::Plaintext,
2238 explicit_plaintext_opt_in: false,
2239 ..NetworkStackConfig::default()
2240 };
2241 let err = cfg.validate_security().unwrap_err();
2242 assert!(matches!(err, FrankenError::Unsupported));
2243
2244 let opted_in = NetworkStackConfig::plaintext_local_dev(true).unwrap();
2245 assert_eq!(opted_in.security, TransportSecurityMode::Plaintext);
2246 assert!(opted_in.validate_security().is_ok());
2247 }
2248
2249 #[test]
2250 fn test_http2_max_concurrent_streams() {
2251 let cfg = NetworkStackConfig::default();
2252 assert!(
2253 cfg.validate_concurrent_streams(DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS)
2254 .is_ok()
2255 );
2256 let err = cfg
2257 .validate_concurrent_streams(DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS + 1)
2258 .unwrap_err();
2259 assert!(matches!(err, FrankenError::Busy));
2260 }
2261
2262 #[test]
2263 fn test_http2_max_header_list_size() {
2264 let cfg = NetworkStackConfig::default();
2265 assert!(
2266 cfg.validate_header_list_size(DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE)
2267 .is_ok()
2268 );
2269 let err = cfg
2270 .validate_header_list_size(DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE + 1)
2271 .unwrap_err();
2272 assert!(matches!(err, FrankenError::TooBig));
2273 }
2274
2275 #[test]
2276 fn test_http2_continuation_timeout() {
2277 let cfg = NetworkStackConfig::default();
2278 assert!(
2279 cfg.validate_continuation_elapsed(DEFAULT_HTTP2_CONTINUATION_TIMEOUT_MS)
2280 .is_ok()
2281 );
2282 let err = cfg
2283 .validate_continuation_elapsed(DEFAULT_HTTP2_CONTINUATION_TIMEOUT_MS + 1)
2284 .unwrap_err();
2285 assert!(matches!(err, FrankenError::BusyRecovery));
2286 }
2287
2288 #[test]
2289 fn test_message_size_cap_enforced() {
2290 let cfg = NetworkStackConfig::default();
2291 assert!(
2292 cfg.validate_message_size(DEFAULT_RPC_MESSAGE_CAP_BYTES)
2293 .is_ok()
2294 );
2295 let err = cfg
2296 .validate_message_size(DEFAULT_RPC_MESSAGE_CAP_BYTES + 1)
2297 .unwrap_err();
2298 assert!(matches!(err, FrankenError::TooBig));
2299 }
2300
2301 #[test]
2302 fn test_handshake_timeout_bounded() {
2303 let cfg = NetworkStackConfig {
2304 handshake_timeout_ms: DEFAULT_HANDSHAKE_TIMEOUT_MS,
2305 ..NetworkStackConfig::default()
2306 };
2307 assert!(
2308 cfg.validate_handshake_elapsed(DEFAULT_HANDSHAKE_TIMEOUT_MS)
2309 .is_ok()
2310 );
2311 let err = cfg
2312 .validate_handshake_elapsed(DEFAULT_HANDSHAKE_TIMEOUT_MS + 500)
2313 .unwrap_err();
2314 assert!(matches!(err, FrankenError::BusyRecovery));
2315 }
2316
2317 #[test]
2318 fn test_virtual_tcp_deterministic() {
2319 let faults = VirtualTcpFaultProfile {
2320 drop_per_million: 150_000,
2321 reorder_per_million: 200_000,
2322 corrupt_per_million: 125_000,
2323 };
2324 let payloads = vec![
2325 b"alpha".to_vec(),
2326 b"beta".to_vec(),
2327 b"gamma".to_vec(),
2328 b"delta".to_vec(),
2329 b"epsilon".to_vec(),
2330 ];
2331
2332 let mut left = VirtualTcp::new(42, faults).unwrap();
2333 let mut left_out = Vec::new();
2334 for payload in &payloads {
2335 left_out.extend(left.transmit(payload));
2336 }
2337 if let Some(flush) = left.flush() {
2338 left_out.push(flush);
2339 }
2340 let left_trace = left.trace().to_vec();
2341
2342 let mut right = VirtualTcp::new(42, faults).unwrap();
2343 let mut right_out = Vec::new();
2344 for payload in &payloads {
2345 right_out.extend(right.transmit(payload));
2346 }
2347 if let Some(flush) = right.flush() {
2348 right_out.push(flush);
2349 }
2350 let right_trace = right.trace().to_vec();
2351
2352 assert_eq!(left_out, right_out);
2353 assert_eq!(left_trace, right_trace);
2354 }
2355
2356 #[test]
2357 fn test_virtual_tcp_fault_injection() {
2358 let mut vtcp = VirtualTcp::new(
2359 7,
2360 VirtualTcpFaultProfile {
2361 drop_per_million: 0,
2362 reorder_per_million: 1_000_000,
2363 corrupt_per_million: 1_000_000,
2364 },
2365 )
2366 .unwrap();
2367
2368 let out_first = vtcp.transmit(b"packet-a");
2369 assert!(out_first.is_empty(), "first packet must be buffered");
2370
2371 let out_second = vtcp.transmit(b"packet-b");
2372 assert_eq!(out_second.len(), 2, "second transmit flushes reorder queue");
2373 assert_ne!(
2374 out_second[0],
2375 b"packet-b".to_vec(),
2376 "corruption must alter delivered payload"
2377 );
2378
2379 let has_buffer = vtcp
2380 .trace()
2381 .iter()
2382 .any(|event| event.kind == VirtualTcpTraceKind::BufferedForReorder);
2383 let has_corrupt_delivery = vtcp
2384 .trace()
2385 .iter()
2386 .any(|event| event.kind == VirtualTcpTraceKind::DeliveredCorrupt);
2387 let has_flush = vtcp
2388 .trace()
2389 .iter()
2390 .any(|event| event.kind == VirtualTcpTraceKind::FlushedReordered);
2391
2392 assert!(has_buffer);
2393 assert!(has_corrupt_delivery);
2394 assert!(has_flush);
2395 }
2396}