1use serde::{Deserialize, Serialize};
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::Arc;
40use thiserror::Error;
41
42#[derive(Debug, Error)]
48pub enum HandshakeError {
49 #[error("Incompatible protocol version: local={local}, remote={remote}")]
51 IncompatibleVersion { local: String, remote: String },
52
53 #[error("No common features between peers")]
55 NoCommonFeatures,
56
57 #[error("Invalid handshake offer: {0}")]
59 InvalidOffer(String),
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub struct ProtocolVersion {
72 pub major: u16,
73 pub minor: u16,
74 pub patch: u16,
75}
76
77impl ProtocolVersion {
78 pub fn new(major: u16, minor: u16, patch: u16) -> Self {
80 Self {
81 major,
82 minor,
83 patch,
84 }
85 }
86
87 pub fn is_compatible_with(&self, other: &Self) -> bool {
92 self.major == other.major
93 }
94}
95
96impl std::fmt::Display for ProtocolVersion {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
99 }
100}
101
102impl PartialOrd for ProtocolVersion {
103 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
104 Some(self.cmp(other))
105 }
106}
107
108impl Ord for ProtocolVersion {
109 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
110 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122pub enum FeatureFlag {
123 Encryption,
124 Compression,
125 VectorSearch,
126 TensorLogic,
127 GradientSync,
128 WebRtc,
129}
130
131impl FeatureFlag {
132 pub const ALL: &'static [Self] = &[
134 Self::Encryption,
135 Self::Compression,
136 Self::VectorSearch,
137 Self::TensorLogic,
138 Self::GradientSync,
139 Self::WebRtc,
140 ];
141
142 pub fn flag_bit(&self) -> u32 {
153 match self {
154 Self::Encryption => 0,
155 Self::Compression => 1,
156 Self::VectorSearch => 2,
157 Self::TensorLogic => 3,
158 Self::GradientSync => 4,
159 Self::WebRtc => 5,
160 }
161 }
162
163 pub fn from_bits(bits: u32) -> Vec<Self> {
167 Self::ALL
168 .iter()
169 .filter(|flag| (bits >> flag.flag_bit()) & 1 == 1)
170 .copied()
171 .collect()
172 }
173}
174
175pub const DEFAULT_MAX_FRAME_SIZE: u32 = 4 * 1024 * 1024;
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct HandshakeOffer {
186 pub peer_id: String,
188 pub protocol_version: ProtocolVersion,
190 pub supported_features: Vec<FeatureFlag>,
192 pub max_frame_size: u32,
194 pub timestamp_ms: u64,
196}
197
198impl HandshakeOffer {
199 pub const DEFAULT_MAX_FRAME_SIZE: u32 = DEFAULT_MAX_FRAME_SIZE;
201
202 pub fn feature_bits(&self) -> u32 {
204 self.supported_features
205 .iter()
206 .fold(0u32, |acc, flag| acc | (1 << flag.flag_bit()))
207 }
208
209 pub(crate) fn validate(&self) -> Result<(), HandshakeError> {
211 if self.peer_id.is_empty() {
212 return Err(HandshakeError::InvalidOffer(
213 "peer_id must not be empty".to_string(),
214 ));
215 }
216 if self.max_frame_size == 0 {
217 return Err(HandshakeError::InvalidOffer(
218 "max_frame_size must be greater than zero".to_string(),
219 ));
220 }
221 Ok(())
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct HandshakeResult {
232 pub agreed_version: ProtocolVersion,
234 pub negotiated_features: Vec<FeatureFlag>,
236 pub max_frame_size: u32,
238 pub local_peer_id: String,
240 pub remote_peer_id: String,
242 pub negotiated_at_ms: u64,
244}
245
246#[derive(Debug, Default)]
252pub struct HandshakeStats {
253 pub total_attempted: AtomicU64,
255 pub total_succeeded: AtomicU64,
257 pub total_failed: AtomicU64,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub struct HandshakeStatsSnapshot {
264 pub total_attempted: u64,
265 pub total_succeeded: u64,
266 pub total_failed: u64,
267}
268
269impl HandshakeStats {
270 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn snapshot(&self) -> HandshakeStatsSnapshot {
277 HandshakeStatsSnapshot {
278 total_attempted: self.total_attempted.load(Ordering::Relaxed),
279 total_succeeded: self.total_succeeded.load(Ordering::Relaxed),
280 total_failed: self.total_failed.load(Ordering::Relaxed),
281 }
282 }
283}
284
285#[derive(Debug)]
300pub struct ProtocolHandshaker {
301 pub local_offer: HandshakeOffer,
303 pub stats: Arc<HandshakeStats>,
305}
306
307impl ProtocolHandshaker {
308 pub fn new(local_offer: HandshakeOffer) -> Self {
310 Self {
311 local_offer,
312 stats: Arc::new(HandshakeStats::new()),
313 }
314 }
315
316 pub fn local_features(&self) -> Vec<FeatureFlag> {
318 self.local_offer.supported_features.clone()
319 }
320
321 pub fn negotiate(&self, remote: &HandshakeOffer) -> Result<HandshakeResult, HandshakeError> {
336 self.stats.total_attempted.fetch_add(1, Ordering::Relaxed);
337
338 if let Err(e) = self.local_offer.validate() {
340 self.stats.total_failed.fetch_add(1, Ordering::Relaxed);
341 return Err(e);
342 }
343 if let Err(e) = remote.validate() {
344 self.stats.total_failed.fetch_add(1, Ordering::Relaxed);
345 return Err(e);
346 }
347
348 let local_ver = &self.local_offer.protocol_version;
350 let remote_ver = &remote.protocol_version;
351 if !local_ver.is_compatible_with(remote_ver) {
352 self.stats.total_failed.fetch_add(1, Ordering::Relaxed);
353 return Err(HandshakeError::IncompatibleVersion {
354 local: local_ver.to_string(),
355 remote: remote_ver.to_string(),
356 });
357 }
358
359 let local_bits = self.local_offer.feature_bits();
361 let remote_bits = remote.feature_bits();
362 let common_bits = local_bits & remote_bits;
363
364 if local_bits != 0 && remote_bits != 0 && common_bits == 0 {
369 self.stats.total_failed.fetch_add(1, Ordering::Relaxed);
370 return Err(HandshakeError::NoCommonFeatures);
371 }
372
373 let negotiated_features = FeatureFlag::from_bits(common_bits);
374
375 let agreed_version = std::cmp::min(local_ver, remote_ver).clone();
377
378 let max_frame_size = std::cmp::min(self.local_offer.max_frame_size, remote.max_frame_size);
380
381 let negotiated_at_ms = std::time::SystemTime::now()
384 .duration_since(std::time::UNIX_EPOCH)
385 .map(|d| d.as_millis() as u64)
386 .unwrap_or(0);
387
388 self.stats.total_succeeded.fetch_add(1, Ordering::Relaxed);
389
390 Ok(HandshakeResult {
391 agreed_version,
392 negotiated_features,
393 max_frame_size,
394 local_peer_id: self.local_offer.peer_id.clone(),
395 remote_peer_id: remote.peer_id.clone(),
396 negotiated_at_ms,
397 })
398 }
399}
400
401#[cfg(test)]
406mod tests {
407 use super::*;
408
409 fn v(major: u16, minor: u16, patch: u16) -> ProtocolVersion {
412 ProtocolVersion::new(major, minor, patch)
413 }
414
415 fn make_offer(
416 peer_id: &str,
417 version: ProtocolVersion,
418 features: Vec<FeatureFlag>,
419 max_frame_size: u32,
420 ) -> HandshakeOffer {
421 HandshakeOffer {
422 peer_id: peer_id.to_string(),
423 protocol_version: version,
424 supported_features: features,
425 max_frame_size,
426 timestamp_ms: 0,
427 }
428 }
429
430 #[test]
433 fn compatible_same_major_same_minor() {
434 let a = v(1, 0, 0);
435 let b = v(1, 0, 0);
436 assert!(a.is_compatible_with(&b));
437 }
438
439 #[test]
440 fn compatible_same_major_different_minor() {
441 let a = v(1, 0, 0);
442 let b = v(1, 5, 3);
443 assert!(a.is_compatible_with(&b));
444 }
445
446 #[test]
447 fn incompatible_different_major() {
448 let a = v(1, 0, 0);
449 let b = v(2, 0, 0);
450 assert!(!a.is_compatible_with(&b));
451 }
452
453 #[test]
454 fn incompatible_major_zero_vs_one() {
455 let a = v(0, 9, 9);
456 let b = v(1, 0, 0);
457 assert!(!a.is_compatible_with(&b));
458 }
459
460 #[test]
463 fn version_ordering_major_takes_precedence() {
464 assert!(v(2, 0, 0) > v(1, 99, 99));
465 }
466
467 #[test]
468 fn version_ordering_minor_within_same_major() {
469 assert!(v(1, 3, 0) > v(1, 2, 99));
470 }
471
472 #[test]
473 fn version_ordering_patch_within_same_major_minor() {
474 assert!(v(1, 0, 5) > v(1, 0, 4));
475 }
476
477 #[test]
478 fn version_ordering_equal() {
479 assert_eq!(v(1, 2, 3), v(1, 2, 3));
480 assert!((v(1, 2, 3) <= v(1, 2, 3)));
481 }
482
483 #[test]
486 fn version_display() {
487 assert_eq!(v(1, 2, 3).to_string(), "1.2.3");
488 assert_eq!(v(0, 0, 0).to_string(), "0.0.0");
489 }
490
491 #[test]
494 fn feature_flag_bit_values() {
495 assert_eq!(FeatureFlag::Encryption.flag_bit(), 0);
496 assert_eq!(FeatureFlag::Compression.flag_bit(), 1);
497 assert_eq!(FeatureFlag::VectorSearch.flag_bit(), 2);
498 assert_eq!(FeatureFlag::TensorLogic.flag_bit(), 3);
499 assert_eq!(FeatureFlag::GradientSync.flag_bit(), 4);
500 assert_eq!(FeatureFlag::WebRtc.flag_bit(), 5);
501 }
502
503 #[test]
506 fn feature_flag_from_bits_roundtrip_all() {
507 let all_bits: u32 = FeatureFlag::ALL
508 .iter()
509 .fold(0, |acc, f| acc | (1 << f.flag_bit()));
510 let decoded = FeatureFlag::from_bits(all_bits);
511 for flag in FeatureFlag::ALL {
513 assert!(
514 decoded.contains(flag),
515 "{:?} missing from decoded set",
516 flag
517 );
518 }
519 }
520
521 #[test]
522 fn feature_flag_from_bits_subset() {
523 let bits: u32 = (1 << 0) | (1 << 2);
525 let flags = FeatureFlag::from_bits(bits);
526 assert_eq!(flags.len(), 2);
527 assert!(flags.contains(&FeatureFlag::Encryption));
528 assert!(flags.contains(&FeatureFlag::VectorSearch));
529 }
530
531 #[test]
532 fn feature_flag_from_bits_zero() {
533 let flags = FeatureFlag::from_bits(0);
534 assert!(flags.is_empty());
535 }
536
537 #[test]
540 fn offer_feature_bits_encoding() {
541 let offer = make_offer(
542 "peer",
543 v(1, 0, 0),
544 vec![FeatureFlag::Encryption, FeatureFlag::GradientSync],
545 DEFAULT_MAX_FRAME_SIZE,
546 );
547 assert_eq!(offer.feature_bits(), (1 << 0) | (1 << 4));
549 }
550
551 #[test]
552 fn offer_feature_bits_empty() {
553 let offer = make_offer("peer", v(1, 0, 0), vec![], DEFAULT_MAX_FRAME_SIZE);
554 assert_eq!(offer.feature_bits(), 0);
555 }
556
557 #[test]
560 fn negotiate_success_compatible_versions() {
561 let local = make_offer(
562 "local",
563 v(1, 0, 0),
564 vec![FeatureFlag::Encryption, FeatureFlag::Compression],
565 DEFAULT_MAX_FRAME_SIZE,
566 );
567 let handshaker = ProtocolHandshaker::new(local);
568 let remote = make_offer(
569 "remote",
570 v(1, 2, 0),
571 vec![FeatureFlag::Encryption, FeatureFlag::VectorSearch],
572 DEFAULT_MAX_FRAME_SIZE,
573 );
574 let result = handshaker
575 .negotiate(&remote)
576 .expect("negotiate should succeed");
577 assert_eq!(result.local_peer_id, "local");
578 assert_eq!(result.remote_peer_id, "remote");
579 assert_eq!(result.agreed_version, v(1, 0, 0));
581 }
582
583 #[test]
586 fn negotiate_fails_incompatible_major() {
587 let local = make_offer(
588 "local",
589 v(1, 0, 0),
590 vec![FeatureFlag::Encryption],
591 DEFAULT_MAX_FRAME_SIZE,
592 );
593 let handshaker = ProtocolHandshaker::new(local);
594 let remote = make_offer(
595 "remote",
596 v(2, 0, 0),
597 vec![FeatureFlag::Encryption],
598 DEFAULT_MAX_FRAME_SIZE,
599 );
600 match handshaker.negotiate(&remote) {
601 Err(HandshakeError::IncompatibleVersion { local, remote }) => {
602 assert_eq!(local, "1.0.0");
603 assert_eq!(remote, "2.0.0");
604 }
605 other => panic!("expected IncompatibleVersion, got {:?}", other),
606 }
607 }
608
609 #[test]
612 fn negotiate_feature_intersection() {
613 let local = make_offer(
614 "local",
615 v(1, 0, 0),
616 vec![
617 FeatureFlag::Encryption,
618 FeatureFlag::Compression,
619 FeatureFlag::VectorSearch,
620 ],
621 DEFAULT_MAX_FRAME_SIZE,
622 );
623 let handshaker = ProtocolHandshaker::new(local);
624 let remote = make_offer(
625 "remote",
626 v(1, 0, 0),
627 vec![FeatureFlag::Encryption, FeatureFlag::TensorLogic],
628 DEFAULT_MAX_FRAME_SIZE,
629 );
630 let result = handshaker
631 .negotiate(&remote)
632 .expect("negotiate should succeed");
633 assert_eq!(result.negotiated_features.len(), 1);
634 assert!(result
635 .negotiated_features
636 .contains(&FeatureFlag::Encryption));
637 }
638
639 #[test]
642 fn negotiate_no_common_features() {
643 let local = make_offer(
644 "local",
645 v(1, 0, 0),
646 vec![FeatureFlag::Compression],
647 DEFAULT_MAX_FRAME_SIZE,
648 );
649 let handshaker = ProtocolHandshaker::new(local);
650 let remote = make_offer(
651 "remote",
652 v(1, 0, 0),
653 vec![FeatureFlag::VectorSearch],
654 DEFAULT_MAX_FRAME_SIZE,
655 );
656 assert!(matches!(
657 handshaker.negotiate(&remote),
658 Err(HandshakeError::NoCommonFeatures)
659 ));
660 }
661
662 #[test]
665 fn negotiate_frame_size_takes_minimum() {
666 let local = make_offer(
667 "local",
668 v(1, 0, 0),
669 vec![FeatureFlag::Encryption],
670 8 * 1024 * 1024,
671 );
672 let handshaker = ProtocolHandshaker::new(local);
673 let remote = make_offer(
674 "remote",
675 v(1, 0, 0),
676 vec![FeatureFlag::Encryption],
677 2 * 1024 * 1024,
678 );
679 let result = handshaker
680 .negotiate(&remote)
681 .expect("negotiate should succeed");
682 assert_eq!(result.max_frame_size, 2 * 1024 * 1024);
683 }
684
685 #[test]
686 fn negotiate_frame_size_local_smaller() {
687 let local = make_offer("local", v(1, 0, 0), vec![FeatureFlag::Encryption], 1024);
688 let handshaker = ProtocolHandshaker::new(local);
689 let remote = make_offer(
690 "remote",
691 v(1, 0, 0),
692 vec![FeatureFlag::Encryption],
693 4 * 1024 * 1024,
694 );
695 let result = handshaker
696 .negotiate(&remote)
697 .expect("negotiate should succeed");
698 assert_eq!(result.max_frame_size, 1024);
699 }
700
701 #[test]
704 fn stats_accumulate_success() {
705 let local = make_offer(
706 "local",
707 v(1, 0, 0),
708 vec![FeatureFlag::Encryption],
709 DEFAULT_MAX_FRAME_SIZE,
710 );
711 let handshaker = ProtocolHandshaker::new(local);
712 let remote = make_offer(
713 "remote",
714 v(1, 0, 0),
715 vec![FeatureFlag::Encryption],
716 DEFAULT_MAX_FRAME_SIZE,
717 );
718
719 handshaker
720 .negotiate(&remote)
721 .expect("negotiate should succeed");
722 handshaker
723 .negotiate(&remote)
724 .expect("negotiate should succeed");
725
726 let snap = handshaker.stats.snapshot();
727 assert_eq!(snap.total_attempted, 2);
728 assert_eq!(snap.total_succeeded, 2);
729 assert_eq!(snap.total_failed, 0);
730 }
731
732 #[test]
733 fn stats_accumulate_failure() {
734 let local = make_offer(
735 "local",
736 v(1, 0, 0),
737 vec![FeatureFlag::Encryption],
738 DEFAULT_MAX_FRAME_SIZE,
739 );
740 let handshaker = ProtocolHandshaker::new(local);
741 let remote = make_offer(
743 "remote",
744 v(2, 0, 0),
745 vec![FeatureFlag::Encryption],
746 DEFAULT_MAX_FRAME_SIZE,
747 );
748
749 let _ = handshaker.negotiate(&remote);
750 let _ = handshaker.negotiate(&remote);
751
752 let snap = handshaker.stats.snapshot();
753 assert_eq!(snap.total_attempted, 2);
754 assert_eq!(snap.total_succeeded, 0);
755 assert_eq!(snap.total_failed, 2);
756 }
757
758 #[test]
761 fn negotiate_invalid_offer_empty_peer_id() {
762 let local = make_offer(
763 "local",
764 v(1, 0, 0),
765 vec![FeatureFlag::Encryption],
766 DEFAULT_MAX_FRAME_SIZE,
767 );
768 let handshaker = ProtocolHandshaker::new(local);
769 let bad_remote = make_offer(
770 "",
771 v(1, 0, 0),
772 vec![FeatureFlag::Encryption],
773 DEFAULT_MAX_FRAME_SIZE,
774 );
775 assert!(matches!(
776 handshaker.negotiate(&bad_remote),
777 Err(HandshakeError::InvalidOffer(_))
778 ));
779 }
780
781 #[test]
784 fn negotiate_both_zero_features_succeeds() {
785 let local = make_offer("local", v(1, 0, 0), vec![], DEFAULT_MAX_FRAME_SIZE);
790 let handshaker = ProtocolHandshaker::new(local);
791 let remote = make_offer("remote", v(1, 0, 0), vec![], DEFAULT_MAX_FRAME_SIZE);
792 let result = handshaker
793 .negotiate(&remote)
794 .expect("negotiate should succeed");
795 assert!(result.negotiated_features.is_empty());
796 }
797
798 #[test]
801 fn local_features_returns_offer_features() {
802 let local = make_offer(
803 "local",
804 v(1, 0, 0),
805 vec![FeatureFlag::Encryption, FeatureFlag::WebRtc],
806 DEFAULT_MAX_FRAME_SIZE,
807 );
808 let handshaker = ProtocolHandshaker::new(local);
809 let features = handshaker.local_features();
810 assert_eq!(features.len(), 2);
811 assert!(features.contains(&FeatureFlag::Encryption));
812 assert!(features.contains(&FeatureFlag::WebRtc));
813 }
814
815 #[test]
818 fn negotiate_agreed_version_is_lower() {
819 let local = make_offer(
821 "local",
822 v(1, 3, 0),
823 vec![FeatureFlag::Encryption],
824 DEFAULT_MAX_FRAME_SIZE,
825 );
826 let handshaker = ProtocolHandshaker::new(local);
827 let remote = make_offer(
828 "remote",
829 v(1, 1, 0),
830 vec![FeatureFlag::Encryption],
831 DEFAULT_MAX_FRAME_SIZE,
832 );
833 let result = handshaker
834 .negotiate(&remote)
835 .expect("negotiate should succeed");
836 assert_eq!(result.agreed_version, v(1, 1, 0));
837 }
838}