1use std::collections::HashMap;
34
35use chia_bls::SecretKey;
36use chia_protocol::Bytes32;
37
38use crate::constants::MAX_CONCURRENT_STREAMS;
39use crate::envelope::{DigMessageEnvelope, InteractionShape, StreamFrame, StreamHeader};
40use crate::error::{MessageError, Result};
41use crate::replay::ReplayGuard;
42use crate::seal::{open_message, seal_message, SealParams};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum Role {
48 Initiator,
49 Responder,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54enum Handshake {
55 Opening,
56 Established,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum StreamState {
62 Opening,
64 Open,
66 HalfClosedLocal,
68 HalfClosedRemote,
70 Closed,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77enum Accepted {
78 Established,
80 Data,
82 Credit(u64),
84 RemoteClosed,
86 CloseAcked,
88 Reset,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum StreamEvent {
95 Opened,
98 Established,
100 Data(Vec<u8>),
102 CreditGranted(u64),
104 RemoteClosed,
106 CloseAcked,
108 PeerReset,
110}
111
112#[derive(Debug, PartialEq, Eq)]
121pub enum StreamAccept {
122 Event(StreamEvent),
124 Dropped {
130 cause: MessageError,
132 },
133 Reset {
139 frame: Box<DigMessageEnvelope>,
141 cause: MessageError,
143 },
144}
145
146#[derive(Debug)]
150pub struct StreamSession {
151 role: Role,
152 handshake: Handshake,
153 send_seq: u64,
155 recv_seq: u64,
158 send_credit: u64,
160 recv_window_remaining: u64,
162 local_closed: bool,
164 remote_closed: bool,
166 reset: bool,
168}
169
170impl StreamSession {
171 fn initiator(recv_window: u32) -> Self {
174 Self {
175 role: Role::Initiator,
176 handshake: Handshake::Opening,
177 send_seq: 0,
178 recv_seq: 0,
179 send_credit: 0,
180 recv_window_remaining: u64::from(recv_window),
181 local_closed: false,
182 remote_closed: false,
183 reset: false,
184 }
185 }
186
187 fn responder(granted_credit: u32) -> Self {
190 Self {
191 role: Role::Responder,
192 handshake: Handshake::Opening,
193 send_seq: 0,
194 recv_seq: 0,
195 send_credit: u64::from(granted_credit),
196 recv_window_remaining: 0,
197 local_closed: false,
198 remote_closed: false,
199 reset: false,
200 }
201 }
202
203 #[must_use]
205 pub fn state(&self) -> StreamState {
206 if self.reset || (self.local_closed && self.remote_closed) {
207 return StreamState::Closed;
208 }
209 if self.handshake == Handshake::Opening {
210 return StreamState::Opening;
211 }
212 match (self.local_closed, self.remote_closed) {
213 (true, false) => StreamState::HalfClosedLocal,
214 (false, true) => StreamState::HalfClosedRemote,
215 _ => StreamState::Open,
216 }
217 }
218
219 #[must_use]
221 pub fn is_closed(&self) -> bool {
222 self.state() == StreamState::Closed
223 }
224
225 #[must_use]
227 pub fn send_credit(&self) -> u64 {
228 self.send_credit
229 }
230
231 fn build_open_ack(&mut self, recv_window: u32) -> Result<StreamHeader> {
237 if self.role != Role::Responder || self.handshake != Handshake::Opening {
238 return Err(MessageError::StreamProtocol(
239 "OPEN_ACK only from a responder mid-handshake",
240 ));
241 }
242 self.handshake = Handshake::Established;
243 self.recv_window_remaining = u64::from(recv_window);
244 Ok(header(StreamFrame::OpenAck, 0, recv_window))
245 }
246
247 fn build_data(&mut self) -> Result<StreamHeader> {
253 if self.handshake != Handshake::Established {
254 return Err(MessageError::StreamProtocol(
255 "DATA before the stream is established",
256 ));
257 }
258 if self.local_closed {
259 return Err(MessageError::StreamProtocol("DATA after our own CLOSE"));
260 }
261 if self.send_credit == 0 {
262 return Err(MessageError::StreamProtocol(
263 "DATA exceeds the granted credit window",
264 ));
265 }
266 self.send_credit -= 1;
267 let seq = self.send_seq;
268 self.send_seq += 1;
269 Ok(header(StreamFrame::Data, seq, 0))
270 }
271
272 fn build_credit(&mut self, n: u32) -> Result<StreamHeader> {
278 if self.handshake != Handshake::Established {
279 return Err(MessageError::StreamProtocol(
280 "CREDIT before the stream is established",
281 ));
282 }
283 self.recv_window_remaining = self.recv_window_remaining.saturating_add(u64::from(n));
284 Ok(header(StreamFrame::Credit, 0, n))
285 }
286
287 fn build_close(&mut self) -> Result<StreamHeader> {
292 if self.local_closed {
293 return Err(MessageError::StreamProtocol("CLOSE after our own CLOSE"));
294 }
295 self.local_closed = true;
296 Ok(header(StreamFrame::Close, 0, 0))
297 }
298
299 fn build_reset(&mut self) -> StreamHeader {
301 self.reset = true;
302 header(StreamFrame::Reset, 0, 0)
303 }
304
305 fn on_recv(&mut self, frame: StreamFrame, hdr: StreamHeader) -> Result<Accepted> {
314 if self.reset {
315 return Err(MessageError::StreamProtocol("frame after RESET"));
316 }
317 match frame {
318 StreamFrame::Open => Err(MessageError::StreamProtocol(
319 "duplicate OPEN for a live stream",
320 )),
321 StreamFrame::OpenAck => {
322 if self.role != Role::Initiator || self.handshake != Handshake::Opening {
323 return Err(MessageError::StreamProtocol("unexpected OPEN_ACK"));
324 }
325 self.handshake = Handshake::Established;
326 self.send_credit = u64::from(hdr.window);
327 Ok(Accepted::Established)
328 }
329 StreamFrame::Data => {
330 if self.handshake != Handshake::Established {
331 return Err(MessageError::StreamProtocol(
332 "DATA before the stream is established",
333 ));
334 }
335 if self.remote_closed {
336 return Err(MessageError::StreamProtocol("DATA after the peer's CLOSE"));
337 }
338 if hdr.seq != self.recv_seq {
339 return Err(MessageError::StreamProtocol(
340 "out-of-order / gap / replayed DATA seq",
341 ));
342 }
343 if self.recv_window_remaining == 0 {
344 return Err(MessageError::StreamProtocol(
345 "DATA exceeds the credit window we granted",
346 ));
347 }
348 self.recv_window_remaining -= 1;
349 self.recv_seq += 1;
350 Ok(Accepted::Data)
351 }
352 StreamFrame::Credit => {
353 if self.handshake != Handshake::Established {
354 return Err(MessageError::StreamProtocol(
355 "CREDIT before the stream is established",
356 ));
357 }
358 self.send_credit = self.send_credit.saturating_add(u64::from(hdr.window));
359 Ok(Accepted::Credit(u64::from(hdr.window)))
360 }
361 StreamFrame::Close => {
362 if self.handshake != Handshake::Established {
363 return Err(MessageError::StreamProtocol(
364 "CLOSE before the stream is established",
365 ));
366 }
367 if self.remote_closed {
368 return Err(MessageError::StreamProtocol(
369 "duplicate CLOSE from the peer",
370 ));
371 }
372 self.remote_closed = true;
373 Ok(Accepted::RemoteClosed)
374 }
375 StreamFrame::CloseAck => {
376 if !self.local_closed {
377 return Err(MessageError::StreamProtocol("CLOSE_ACK without our CLOSE"));
378 }
379 Ok(Accepted::CloseAcked)
380 }
381 StreamFrame::Reset => {
382 self.reset = true;
383 Ok(Accepted::Reset)
384 }
385 }
386 }
387}
388
389fn header(frame: StreamFrame, seq: u64, window: u32) -> StreamHeader {
391 StreamHeader {
392 frame: frame.as_u8(),
393 seq,
394 window,
395 }
396}
397
398pub struct StreamEndpoint<'a> {
406 identity_sk: &'a SecretKey,
408 local_did: Bytes32,
410 local_epoch: u32,
412 peer_did: Bytes32,
414 peer_pub: &'a [u8; 48],
416 message_type: u32,
418 send_counter: u64,
420 sessions: HashMap<Bytes32, StreamSession>,
422 max_concurrent: usize,
424 guard: ReplayGuard,
426}
427
428impl<'a> StreamEndpoint<'a> {
429 #[must_use]
434 pub fn new(
435 identity_sk: &'a SecretKey,
436 local_did: Bytes32,
437 local_epoch: u32,
438 peer_did: Bytes32,
439 peer_pub: &'a [u8; 48],
440 message_type: u32,
441 ) -> Self {
442 Self {
443 identity_sk,
444 local_did,
445 local_epoch,
446 peer_did,
447 peer_pub,
448 message_type,
449 send_counter: 0,
450 sessions: HashMap::new(),
451 max_concurrent: MAX_CONCURRENT_STREAMS,
452 guard: ReplayGuard::new(),
453 }
454 }
455
456 #[must_use]
459 pub fn with_max_concurrent(mut self, max: usize) -> Self {
460 self.max_concurrent = max;
461 self
462 }
463
464 #[must_use]
466 pub fn stream_count(&self) -> usize {
467 self.sessions.len()
468 }
469
470 #[must_use]
472 pub fn session(&self, correlation_id: Bytes32) -> Option<&StreamSession> {
473 self.sessions.get(&correlation_id)
474 }
475
476 pub fn open(
483 &mut self,
484 correlation_id: Bytes32,
485 recv_window: u32,
486 now_ms: u64,
487 expires_at: u64,
488 ) -> Result<DigMessageEnvelope> {
489 if self.sessions.contains_key(&correlation_id) {
490 return Err(MessageError::StreamProtocol(
491 "correlation_id already in use",
492 ));
493 }
494 if self.sessions.len() >= self.max_concurrent {
495 return Err(MessageError::StreamLimit {
496 cap: self.max_concurrent,
497 });
498 }
499 self.sessions
500 .insert(correlation_id, StreamSession::initiator(recv_window));
501 let hdr = header(StreamFrame::Open, 0, recv_window);
502 self.seal_frame(correlation_id, hdr, &[], now_ms, expires_at)
503 }
504
505 pub fn open_ack(
512 &mut self,
513 correlation_id: Bytes32,
514 recv_window: u32,
515 now_ms: u64,
516 expires_at: u64,
517 ) -> Result<DigMessageEnvelope> {
518 let hdr = self
519 .session_mut(correlation_id)?
520 .build_open_ack(recv_window)?;
521 self.seal_frame(correlation_id, hdr, &[], now_ms, expires_at)
522 }
523
524 pub fn send_data(
531 &mut self,
532 correlation_id: Bytes32,
533 payload: &[u8],
534 now_ms: u64,
535 expires_at: u64,
536 ) -> Result<DigMessageEnvelope> {
537 let hdr = self.session_mut(correlation_id)?.build_data()?;
538 self.seal_frame(correlation_id, hdr, payload, now_ms, expires_at)
539 }
540
541 pub fn grant_credit(
546 &mut self,
547 correlation_id: Bytes32,
548 n: u32,
549 now_ms: u64,
550 expires_at: u64,
551 ) -> Result<DigMessageEnvelope> {
552 let hdr = self.session_mut(correlation_id)?.build_credit(n)?;
553 self.seal_frame(correlation_id, hdr, &[], now_ms, expires_at)
554 }
555
556 pub fn close(
562 &mut self,
563 correlation_id: Bytes32,
564 now_ms: u64,
565 expires_at: u64,
566 ) -> Result<DigMessageEnvelope> {
567 let hdr = self.session_mut(correlation_id)?.build_close()?;
568 let env = self.seal_frame(correlation_id, hdr, &[], now_ms, expires_at)?;
569 self.drop_if_closed(correlation_id);
570 Ok(env)
571 }
572
573 pub fn reset(
579 &mut self,
580 correlation_id: Bytes32,
581 now_ms: u64,
582 expires_at: u64,
583 ) -> Result<DigMessageEnvelope> {
584 let hdr = self.session_mut(correlation_id)?.build_reset();
585 let env = self.seal_frame(correlation_id, hdr, &[], now_ms, expires_at)?;
586 self.sessions.remove(&correlation_id);
587 Ok(env)
588 }
589
590 pub fn accept(
606 &mut self,
607 envelope: &DigMessageEnvelope,
608 resolve_sender_pub: impl Fn(Bytes32, u32) -> Option<[u8; 48]>,
609 now_ms: u64,
610 ) -> Result<StreamAccept> {
611 let correlation_id = envelope.correlation_id;
612
613 let opened = match open_message(
621 self.identity_sk,
622 envelope,
623 &resolve_sender_pub,
624 &mut self.guard,
625 now_ms,
626 ) {
627 Ok(opened) => opened,
628 Err(cause) => return Ok(StreamAccept::Dropped { cause }),
629 };
630
631 let Some(hdr) = envelope.stream else {
634 return Ok(StreamAccept::Dropped {
635 cause: MessageError::StreamProtocol("stream event on a non-stream envelope"),
636 });
637 };
638 let Some(frame) = StreamFrame::from_u8(hdr.frame) else {
639 return Ok(StreamAccept::Dropped {
640 cause: MessageError::StreamProtocol("unknown stream frame kind"),
641 });
642 };
643 if opened.shape != InteractionShape::StreamFrame {
644 return Ok(StreamAccept::Dropped {
645 cause: MessageError::StreamProtocol("stream frame with a non-stream shape"),
646 });
647 }
648
649 if frame == StreamFrame::Reset {
652 return Ok(if self.sessions.remove(&correlation_id).is_some() {
653 StreamAccept::Event(StreamEvent::PeerReset)
654 } else {
655 StreamAccept::Dropped {
656 cause: MessageError::StreamProtocol("RESET for an unknown stream"),
657 }
658 });
659 }
660
661 if frame == StreamFrame::Open {
663 return self.accept_open(correlation_id, hdr, now_ms);
664 }
665
666 if !self.sessions.contains_key(&correlation_id) {
671 return Ok(StreamAccept::Dropped {
672 cause: MessageError::StreamProtocol("frame for an unknown stream"),
673 });
674 }
675 let transition = self
676 .sessions
677 .get_mut(&correlation_id)
678 .expect("presence checked above")
679 .on_recv(frame, hdr);
680 match transition {
681 Ok(accepted) => {
682 let event = to_event(accepted, opened.payload);
683 self.drop_if_closed(correlation_id);
684 Ok(StreamAccept::Event(event))
685 }
686 Err(cause) => {
687 self.sessions.remove(&correlation_id);
688 self.reset_response(correlation_id, now_ms, cause)
689 }
690 }
691 }
692
693 fn accept_open(
697 &mut self,
698 correlation_id: Bytes32,
699 hdr: StreamHeader,
700 now_ms: u64,
701 ) -> Result<StreamAccept> {
702 if self.sessions.contains_key(&correlation_id) {
703 return self.reset_response(
704 correlation_id,
705 now_ms,
706 MessageError::StreamProtocol("duplicate OPEN for a live stream"),
707 );
708 }
709 if self.sessions.len() >= self.max_concurrent {
710 return self.reset_response(
711 correlation_id,
712 now_ms,
713 MessageError::StreamLimit {
714 cap: self.max_concurrent,
715 },
716 );
717 }
718 self.sessions
719 .insert(correlation_id, StreamSession::responder(hdr.window));
720 Ok(StreamAccept::Event(StreamEvent::Opened))
721 }
722
723 fn reset_response(
728 &mut self,
729 correlation_id: Bytes32,
730 now_ms: u64,
731 cause: MessageError,
732 ) -> Result<StreamAccept> {
733 let hdr = header(StreamFrame::Reset, 0, 0);
734 let frame = self.seal_frame(correlation_id, hdr, &[], now_ms, 0)?;
735 Ok(StreamAccept::Reset {
736 frame: Box::new(frame),
737 cause,
738 })
739 }
740
741 fn seal_frame(
745 &mut self,
746 correlation_id: Bytes32,
747 hdr: StreamHeader,
748 payload: &[u8],
749 now_ms: u64,
750 expires_at: u64,
751 ) -> Result<DigMessageEnvelope> {
752 let counter = self.send_counter;
753 self.send_counter += 1;
754 let params = SealParams {
755 sender_sk: self.identity_sk,
756 sender: self.local_did,
757 sender_epoch: self.local_epoch,
758 recipient: self.peer_did,
759 recipient_pub: self.peer_pub,
760 message_type: self.message_type,
761 shape: InteractionShape::StreamFrame,
762 correlation_id,
763 stream: Some(hdr),
764 counter,
765 timestamp_ms: now_ms,
766 expires_at,
767 payload,
768 };
769 seal_message(¶ms)
770 }
771
772 fn session_mut(&mut self, correlation_id: Bytes32) -> Result<&mut StreamSession> {
774 self.sessions
775 .get_mut(&correlation_id)
776 .ok_or(MessageError::StreamProtocol("unknown stream"))
777 }
778
779 fn drop_if_closed(&mut self, correlation_id: Bytes32) {
781 if self
782 .sessions
783 .get(&correlation_id)
784 .is_some_and(StreamSession::is_closed)
785 {
786 self.sessions.remove(&correlation_id);
787 }
788 }
789}
790
791fn to_event(accepted: Accepted, payload: Vec<u8>) -> StreamEvent {
793 match accepted {
794 Accepted::Established => StreamEvent::Established,
795 Accepted::Data => StreamEvent::Data(payload),
796 Accepted::Credit(n) => StreamEvent::CreditGranted(n),
797 Accepted::RemoteClosed => StreamEvent::RemoteClosed,
798 Accepted::CloseAcked => StreamEvent::CloseAcked,
799 Accepted::Reset => StreamEvent::PeerReset,
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use dig_identity::{derive_identity_sk, master_secret_key_from_seed, public_key_bytes};
807 use sha2::{Digest, Sha256};
808
809 const NOW: u64 = 1_700_000_000_000;
810
811 fn sk(label: &str) -> SecretKey {
812 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
813 derive_identity_sk(&master_secret_key_from_seed(&seed))
814 }
815
816 fn cid(tag: &str) -> Bytes32 {
817 Bytes32::new(Sha256::digest(tag.as_bytes()).into())
818 }
819
820 #[test]
823 fn initiator_handshake_then_data() {
824 let mut s = StreamSession::initiator(4);
825 assert_eq!(s.state(), StreamState::Opening);
826 assert!(s.build_data().is_err());
828 assert_eq!(
830 s.on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 2)),
831 Ok(Accepted::Established)
832 );
833 assert_eq!(s.state(), StreamState::Open);
834 assert_eq!(s.send_credit(), 2);
835 assert!(s.build_data().is_ok());
837 assert!(s.build_data().is_ok());
838 assert!(s.build_data().is_err());
839 }
840
841 #[test]
842 fn outbound_data_seq_increments() {
843 let mut s = StreamSession::initiator(0);
844 s.on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 3))
845 .unwrap();
846 assert_eq!(s.build_data().unwrap().seq, 0);
847 assert_eq!(s.build_data().unwrap().seq, 1);
848 assert_eq!(s.build_data().unwrap().seq, 2);
849 }
850
851 #[test]
852 fn responder_rejects_out_of_order_recv_seq() {
853 let mut s = StreamSession::responder(0);
855 s.build_open_ack(4).unwrap();
856 assert_eq!(
858 s.on_recv(StreamFrame::Data, header(StreamFrame::Data, 0, 0)),
859 Ok(Accepted::Data)
860 );
861 assert!(s
863 .on_recv(StreamFrame::Data, header(StreamFrame::Data, 2, 0))
864 .is_err());
865 assert!(s
867 .on_recv(StreamFrame::Data, header(StreamFrame::Data, 0, 0))
868 .is_err());
869 }
870
871 #[test]
872 fn recv_credit_window_bounds_inbound_data() {
873 let mut s = StreamSession::responder(0);
874 s.build_open_ack(1).unwrap(); assert_eq!(
876 s.on_recv(StreamFrame::Data, header(StreamFrame::Data, 0, 0)),
877 Ok(Accepted::Data)
878 );
879 assert!(s
881 .on_recv(StreamFrame::Data, header(StreamFrame::Data, 1, 0))
882 .is_err());
883 }
884
885 #[test]
886 fn credit_frame_relieves_send_backpressure() {
887 let mut s = StreamSession::initiator(0);
888 s.on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 1))
889 .unwrap();
890 s.build_data().unwrap();
891 assert!(s.build_data().is_err(), "credit exhausted");
892 assert_eq!(
894 s.on_recv(StreamFrame::Credit, header(StreamFrame::Credit, 0, 2)),
895 Ok(Accepted::Credit(2))
896 );
897 assert!(s.build_data().is_ok());
898 assert!(s.build_data().is_ok());
899 assert!(s.build_data().is_err());
900 }
901
902 #[test]
903 fn bidirectional_half_close() {
904 let mut s = StreamSession::initiator(4);
905 s.on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 4))
906 .unwrap();
907 s.build_close().unwrap();
909 assert_eq!(s.state(), StreamState::HalfClosedLocal);
910 assert!(s.build_data().is_err(), "no DATA after our CLOSE");
911 assert_eq!(
913 s.on_recv(StreamFrame::Data, header(StreamFrame::Data, 0, 0)),
914 Ok(Accepted::Data)
915 );
916 assert_eq!(
918 s.on_recv(StreamFrame::Close, header(StreamFrame::Close, 0, 0)),
919 Ok(Accepted::RemoteClosed)
920 );
921 assert_eq!(s.state(), StreamState::Closed);
922 assert!(s.is_closed());
923 }
924
925 #[test]
926 fn reset_aborts_from_any_state() {
927 let mut s = StreamSession::initiator(4);
928 assert_eq!(
929 s.on_recv(StreamFrame::Reset, header(StreamFrame::Reset, 0, 0)),
930 Ok(Accepted::Reset)
931 );
932 assert_eq!(s.state(), StreamState::Closed);
933 assert!(s
935 .on_recv(StreamFrame::Data, header(StreamFrame::Data, 0, 0))
936 .is_err());
937 }
938
939 struct Pair {
942 a_sk: SecretKey,
943 a_did: Bytes32,
944 a_pub: [u8; 48],
945 b_sk: SecretKey,
946 b_did: Bytes32,
947 b_pub: [u8; 48],
948 }
949 fn pair(tag: &str) -> Pair {
950 let a_sk = sk(&format!("{tag}/a"));
951 let b_sk = sk(&format!("{tag}/b"));
952 Pair {
953 a_pub: public_key_bytes(&a_sk),
954 b_pub: public_key_bytes(&b_sk),
955 a_did: cid(&format!("{tag}/a-did")),
956 b_did: cid(&format!("{tag}/b-did")),
957 a_sk,
958 b_sk,
959 }
960 }
961
962 const MT: u32 = 0x0000_0200; #[test]
965 fn full_stream_round_trip_open_data_close() {
966 let p = pair("rt");
967 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
968 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
969 let sender_is_bob = |_d: Bytes32, _e: u32| Some(p.b_pub);
970 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
971 let stream = cid("rt/stream");
972
973 let open = alice.open(stream, 4, NOW, 0).unwrap();
975 assert!(matches!(
976 bob.accept(&open, sender_is_alice, NOW).unwrap(),
977 StreamAccept::Event(StreamEvent::Opened)
978 ));
979
980 let ack = bob.open_ack(stream, 4, NOW, 0).unwrap();
982 assert!(matches!(
983 alice.accept(&ack, sender_is_bob, NOW).unwrap(),
984 StreamAccept::Event(StreamEvent::Established)
985 ));
986
987 let d0 = alice.send_data(stream, b"hello ", NOW, 0).unwrap();
989 let d1 = alice.send_data(stream, b"world", NOW, 0).unwrap();
990 assert_eq!(
991 bob.accept(&d0, sender_is_alice, NOW).unwrap(),
992 StreamAccept::Event(StreamEvent::Data(b"hello ".to_vec()))
993 );
994 assert_eq!(
995 bob.accept(&d1, sender_is_alice, NOW).unwrap(),
996 StreamAccept::Event(StreamEvent::Data(b"world".to_vec()))
997 );
998
999 let close = alice.close(stream, NOW, 0).unwrap();
1001 assert!(matches!(
1002 bob.accept(&close, sender_is_alice, NOW).unwrap(),
1003 StreamAccept::Event(StreamEvent::RemoteClosed)
1004 ));
1005 }
1006
1007 #[test]
1008 fn concurrent_stream_cap_rejects_the_nth_plus_one_open() {
1009 let p = pair("cap");
1010 let mut bob =
1012 StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT).with_max_concurrent(2);
1013 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1014 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1015
1016 for i in 0..2 {
1017 let open = alice.open(cid(&format!("cap/{i}")), 1, NOW, 0).unwrap();
1018 assert!(matches!(
1019 bob.accept(&open, sender_is_alice, NOW).unwrap(),
1020 StreamAccept::Event(StreamEvent::Opened)
1021 ));
1022 }
1023 assert_eq!(bob.stream_count(), 2);
1024
1025 let open3 = alice.open(cid("cap/3"), 1, NOW, 0).unwrap();
1027 match bob.accept(&open3, sender_is_alice, NOW).unwrap() {
1028 StreamAccept::Reset { cause, .. } => {
1029 assert!(matches!(cause, MessageError::StreamLimit { cap: 2 }));
1030 }
1031 other => panic!("expected a StreamLimit RESET, got {other:?}"),
1032 }
1033 assert_eq!(
1034 bob.stream_count(),
1035 2,
1036 "the rejected OPEN created no session"
1037 );
1038 }
1039
1040 #[test]
1041 fn failed_verify_frame_is_dropped_never_reset() {
1042 let p = pair("badverify");
1045 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1046 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1047 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1048 let stream = cid("badverify/s");
1049
1050 let mut open = alice.open(stream, 4, NOW, 0).unwrap();
1051 let last = open.sealed.ciphertext.len() - 1;
1052 open.sealed.ciphertext[last] ^= 0x01;
1053 assert_eq!(
1054 bob.accept(&open, sender_is_alice, NOW).unwrap(),
1055 StreamAccept::Dropped {
1056 cause: MessageError::OpenFailed
1057 }
1058 );
1059 assert_eq!(bob.stream_count(), 0);
1060 }
1061
1062 #[test]
1063 fn frame_for_unknown_stream_is_dropped_never_reset() {
1064 let p = pair("proto");
1065 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1066 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1067 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1068 let stream = cid("proto/s");
1069
1070 alice.open(stream, 4, NOW, 0).unwrap();
1072 alice
1073 .sessions
1074 .get_mut(&stream)
1075 .unwrap()
1076 .on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 4))
1077 .unwrap();
1078 let data = alice.send_data(stream, b"early", NOW, 0).unwrap();
1079 assert!(matches!(
1081 bob.accept(&data, sender_is_alice, NOW).unwrap(),
1082 StreamAccept::Dropped { .. }
1083 ));
1084 }
1085
1086 #[test]
1087 fn replayed_data_on_a_live_stream_is_dropped_and_stream_survives() {
1088 let p = pair("replay");
1091 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1092 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1093 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1094 let stream = cid("replay/s");
1095
1096 let open = alice.open(stream, 4, NOW, 0).unwrap();
1097 bob.accept(&open, sender_is_alice, NOW).unwrap();
1098 let ack = bob.open_ack(stream, 4, NOW, 0).unwrap();
1099 alice.accept(&ack, |_d, _e| Some(p.b_pub), NOW).unwrap();
1100 let data = alice.send_data(stream, b"once", NOW, 0).unwrap();
1101 assert_eq!(
1102 bob.accept(&data, sender_is_alice, NOW).unwrap(),
1103 StreamAccept::Event(StreamEvent::Data(b"once".to_vec()))
1104 );
1105 assert_eq!(
1107 bob.accept(&data, sender_is_alice, NOW).unwrap(),
1108 StreamAccept::Dropped {
1109 cause: MessageError::Replay
1110 }
1111 );
1112 assert_eq!(bob.stream_count(), 1);
1114 assert_eq!(bob.session(stream).unwrap().state(), StreamState::Open);
1115 let next = alice.send_data(stream, b"twice", NOW, 0).unwrap();
1117 assert_eq!(
1118 bob.accept(&next, sender_is_alice, NOW).unwrap(),
1119 StreamAccept::Event(StreamEvent::Data(b"twice".to_vec()))
1120 );
1121 }
1122
1123 #[test]
1124 fn inbound_reset_for_unknown_stream_does_not_beget_a_reset() {
1125 let p = pair("resetstorm");
1127 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1128 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1129 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1130
1131 let ghost = cid("resetstorm/ghost");
1133 alice.open(ghost, 1, NOW, 0).unwrap();
1134 let reset = alice.reset(ghost, NOW, 0).unwrap();
1135 assert!(matches!(
1136 bob.accept(&reset, sender_is_alice, NOW).unwrap(),
1137 StreamAccept::Dropped { .. }
1138 ));
1139 assert_eq!(bob.stream_count(), 0);
1140 }
1141
1142 #[test]
1143 fn protocol_violation_on_established_stream_still_resets() {
1144 let p = pair("legitreset");
1147 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1148 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1149 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1150 let sender_is_bob = |_d: Bytes32, _e: u32| Some(p.b_pub);
1151 let stream = cid("legitreset/s");
1152
1153 let open = alice.open(stream, 4, NOW, 0).unwrap();
1154 bob.accept(&open, sender_is_alice, NOW).unwrap();
1155 let ack = bob.open_ack(stream, 4, NOW, 0).unwrap();
1156 alice.accept(&ack, sender_is_bob, NOW).unwrap();
1157
1158 alice.sessions.get_mut(&stream).unwrap().send_seq = 1;
1160 let bad = alice.send_data(stream, b"gap", NOW, 0).unwrap();
1161 match bob.accept(&bad, sender_is_alice, NOW).unwrap() {
1162 StreamAccept::Reset { cause, .. } => {
1163 assert!(matches!(cause, MessageError::StreamProtocol(_)));
1164 }
1165 other => panic!("expected a RESET on the authenticated violation, got {other:?}"),
1166 }
1167 assert_eq!(bob.stream_count(), 0, "the violated session is dropped");
1168 }
1169
1170 #[test]
1171 fn every_frame_uses_a_unique_ephemeral() {
1172 let p = pair("uniq");
1175 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1176 let stream = cid("uniq/s");
1177 let mut kems = Vec::new();
1178 kems.push(alice.open(stream, 100, NOW, 0).unwrap().sealed.kem_enc);
1179 alice
1180 .sessions
1181 .get_mut(&stream)
1182 .unwrap()
1183 .on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 100))
1184 .unwrap();
1185 for _ in 0..16 {
1186 kems.push(
1187 alice
1188 .send_data(stream, b"x", NOW, 0)
1189 .unwrap()
1190 .sealed
1191 .kem_enc,
1192 );
1193 }
1194 let unique: std::collections::HashSet<_> = kems.iter().collect();
1195 assert_eq!(
1196 unique.len(),
1197 kems.len(),
1198 "every frame must use a fresh ephemeral"
1199 );
1200 }
1201
1202 #[test]
1203 fn credit_grant_close_ack_and_peer_reset_round_trip() {
1204 let p = pair("credit");
1206 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1207 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1208 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1209 let sender_is_bob = |_d: Bytes32, _e: u32| Some(p.b_pub);
1210 let stream = cid("credit/s");
1211
1212 let open = alice.open(stream, 1, NOW, 0).unwrap();
1213 bob.accept(&open, sender_is_alice, NOW).unwrap();
1214 let ack = bob.open_ack(stream, 1, NOW, 0).unwrap();
1215 alice.accept(&ack, sender_is_bob, NOW).unwrap();
1216
1217 let credit = bob.grant_credit(stream, 3, NOW, 0).unwrap();
1219 assert_eq!(
1220 alice.accept(&credit, sender_is_bob, NOW).unwrap(),
1221 StreamAccept::Event(StreamEvent::CreditGranted(3))
1222 );
1223 assert_eq!(alice.session(stream).unwrap().send_credit(), 4);
1224
1225 let reset = bob.reset(stream, NOW, 0).unwrap();
1227 assert_eq!(
1228 alice.accept(&reset, sender_is_bob, NOW).unwrap(),
1229 StreamAccept::Event(StreamEvent::PeerReset)
1230 );
1231 assert_eq!(alice.stream_count(), 0);
1232 assert_eq!(
1233 bob.stream_count(),
1234 0,
1235 "reset drops the sender's session too"
1236 );
1237 }
1238
1239 #[test]
1240 fn close_ack_is_delivered() {
1241 let p = pair("closeack");
1242 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1243 let mut bob = StreamEndpoint::new(&p.b_sk, p.b_did, 0, p.a_did, &p.a_pub, MT);
1244 let sender_is_alice = |_d: Bytes32, _e: u32| Some(p.a_pub);
1245 let sender_is_bob = |_d: Bytes32, _e: u32| Some(p.b_pub);
1246 let stream = cid("closeack/s");
1247
1248 let open = alice.open(stream, 2, NOW, 0).unwrap();
1249 bob.accept(&open, sender_is_alice, NOW).unwrap();
1250 let ack = bob.open_ack(stream, 2, NOW, 0).unwrap();
1251 alice.accept(&ack, sender_is_bob, NOW).unwrap();
1252
1253 let close = alice.close(stream, NOW, 0).unwrap();
1255 bob.accept(&close, sender_is_alice, NOW).unwrap();
1256 let close_ack = bob
1258 .seal_frame(stream, header(StreamFrame::CloseAck, 0, 0), &[], NOW, 0)
1259 .unwrap();
1260 assert_eq!(
1261 alice.accept(&close_ack, sender_is_bob, NOW).unwrap(),
1262 StreamAccept::Event(StreamEvent::CloseAcked)
1263 );
1264 }
1265
1266 #[test]
1267 fn endpoint_send_side_error_branches() {
1268 let p = pair("errs");
1269 let mut alice = StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT);
1270 let stream = cid("errs/s");
1271
1272 assert!(matches!(
1274 alice.send_data(stream, b"x", NOW, 0),
1275 Err(MessageError::StreamProtocol(_))
1276 ));
1277
1278 alice.open(stream, 1, NOW, 0).unwrap();
1279 assert!(matches!(
1281 alice.open(stream, 1, NOW, 0),
1282 Err(MessageError::StreamProtocol(_))
1283 ));
1284
1285 let mut tight =
1287 StreamEndpoint::new(&p.a_sk, p.a_did, 0, p.b_did, &p.b_pub, MT).with_max_concurrent(1);
1288 tight.open(cid("errs/only"), 1, NOW, 0).unwrap();
1289 assert!(matches!(
1290 tight.open(cid("errs/2"), 1, NOW, 0),
1291 Err(MessageError::StreamLimit { cap: 1 })
1292 ));
1293 }
1294
1295 #[test]
1296 fn half_closed_remote_state_then_local_close() {
1297 let mut s = StreamSession::initiator(4);
1299 s.on_recv(StreamFrame::OpenAck, header(StreamFrame::OpenAck, 0, 4))
1300 .unwrap();
1301 s.on_recv(StreamFrame::Close, header(StreamFrame::Close, 0, 0))
1302 .unwrap();
1303 assert_eq!(s.state(), StreamState::HalfClosedRemote);
1304 assert!(
1305 s.build_data().is_ok(),
1306 "we may still send after the peer's CLOSE"
1307 );
1308 s.build_close().unwrap();
1309 assert_eq!(s.state(), StreamState::Closed);
1310 }
1311}