bitfold_peer/unsequenced.rs
1//! Unsequenced packet duplicate detection using a sliding window.
2//!
3//! This module implements a 1024-bit sliding window for detecting duplicate unsequenced packets.
4//! Unsequenced packets can arrive in any order and may be duplicated by the network, so we need
5//! to track which group IDs have already been received to filter out duplicates.
6//!
7//! # Design
8//!
9//! The sliding window uses 1024 bits (32 u32 words) to track received group IDs:
10//! - Each bit represents whether a specific group ID has been received
11//! - The window base (`incoming_unsequenced_group`) tracks the oldest group in the window
12//! - When packets arrive far ahead of the window, the window slides forward
13//! - Old packets (behind the window) are treated as duplicates
14//!
15//! # Wrapping Arithmetic
16//!
17//! Group IDs are u16 values that wrap around at 65536. The window uses wrapping arithmetic
18//! to correctly handle the transition from 65535 to 0.
19
20/// State for tracking unsequenced packet duplicates using a sliding window.
21///
22/// This struct maintains a 1024-bit window for duplicate detection of unsequenced packets.
23/// It tracks which group IDs have been received and automatically slides the window forward
24/// when new packets arrive far ahead of the current window base.
25#[derive(Debug, Clone)]
26pub struct UnsequencedState {
27 /// Incoming unsequenced group base (start of sliding window)
28 incoming_unsequenced_group: u16,
29 /// Unsequenced window for duplicate detection (1024 bits = 128 bytes)
30 /// Each bit represents whether a group has been received in the window
31 unsequenced_window: [u32; 32], // 32 u32s = 1024 bits
32 /// Outgoing unsequenced group counter
33 outgoing_unsequenced_group: u16,
34}
35
36impl Default for UnsequencedState {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl UnsequencedState {
43 /// Creates a new unsequenced state with an empty window.
44 pub fn new() -> Self {
45 Self {
46 incoming_unsequenced_group: 0,
47 unsequenced_window: [0; 32], // All bits start as 0 (no groups received)
48 outgoing_unsequenced_group: 0,
49 }
50 }
51
52 /// Gets the next outgoing unsequenced group ID and increments the counter.
53 pub fn next_outgoing_group(&mut self) -> u16 {
54 let group = self.outgoing_unsequenced_group;
55 self.outgoing_unsequenced_group = self.outgoing_unsequenced_group.wrapping_add(1);
56 group
57 }
58
59 /// Returns the current incoming unsequenced group (window base).
60 pub fn incoming_group(&self) -> u16 {
61 self.incoming_unsequenced_group
62 }
63
64 /// Returns the current outgoing unsequenced group.
65 pub fn outgoing_group(&self) -> u16 {
66 self.outgoing_unsequenced_group
67 }
68
69 /// Checks if an incoming unsequenced group is a duplicate or within the window.
70 /// Returns true if this is a duplicate (already received), false if new.
71 pub fn is_duplicate(&self, group: u16) -> bool {
72 // Check if window is completely empty (no packets received yet)
73 let window_empty = self.unsequenced_window.iter().all(|&w| w == 0);
74 if window_empty {
75 // First packet(s), not a duplicate
76 return false;
77 }
78
79 // Calculate offset from window base
80 let offset = group.wrapping_sub(self.incoming_unsequenced_group);
81
82 // If offset is within window (0-1023), check the window
83 if offset < 1024 {
84 // Check if this bit is set in the window
85 let word_index = (offset / 32) as usize;
86 let bit_index = offset % 32;
87
88 if word_index < 32 {
89 return (self.unsequenced_window[word_index] & (1 << bit_index)) != 0;
90 }
91 }
92
93 // Outside the forward window - check if it's old (behind the window)
94 // Using wrapping arithmetic: if offset > 32768, it's actually a negative offset
95 // meaning it's behind the current window base (old packet)
96 if offset > 32768 {
97 return true; // Old packet, treat as duplicate
98 }
99
100 // New packet ahead of window, not a duplicate
101 false
102 }
103
104 /// Marks an incoming unsequenced group as received in the window.
105 /// Advances the window base if necessary.
106 pub fn mark_received(&mut self, group: u16) {
107 // Calculate offset from window base
108 let offset = group.wrapping_sub(self.incoming_unsequenced_group);
109
110 // If the group is far ahead (and not wrapping backwards), we need to advance the window base
111 if (1024..=32768).contains(&offset) {
112 // Calculate how much to advance
113 let advance = offset.saturating_sub(512); // Keep window centered on new packets
114
115 // Shift window bits and clear old ones
116 let word_shift = (advance / 32) as usize;
117 let bit_shift = advance % 32;
118
119 if word_shift > 0 {
120 // Shift entire words
121 if word_shift >= 32 {
122 // Complete window reset
123 self.unsequenced_window = [0; 32];
124 } else {
125 // Partial shift
126 for i in 0..(32 - word_shift) {
127 self.unsequenced_window[i] = self.unsequenced_window[i + word_shift];
128 }
129 for i in (32 - word_shift)..32 {
130 self.unsequenced_window[i] = 0;
131 }
132 }
133 }
134
135 if bit_shift > 0 && word_shift < 32 {
136 // Shift bits within words
137 for i in 0..31 {
138 self.unsequenced_window[i] = (self.unsequenced_window[i] >> bit_shift)
139 | (self.unsequenced_window[i + 1] << (32 - bit_shift));
140 }
141 self.unsequenced_window[31] >>= bit_shift;
142 }
143
144 // Advance window base
145 self.incoming_unsequenced_group = self.incoming_unsequenced_group.wrapping_add(advance);
146 }
147
148 // Now mark the bit for this group
149 let final_offset = group.wrapping_sub(self.incoming_unsequenced_group);
150 if final_offset < 1024 {
151 let word_index = (final_offset / 32) as usize;
152 let bit_index = final_offset % 32;
153 if word_index < 32 {
154 self.unsequenced_window[word_index] |= 1 << bit_index;
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_unsequenced_window_sliding() {
166 let mut state = UnsequencedState::new();
167
168 // Receive a packet at group 0
169 assert!(!state.is_duplicate(0));
170 state.mark_received(0);
171
172 // Same packet again should be duplicate
173 assert!(state.is_duplicate(0));
174
175 // Receive a packet far ahead (group 1500, outside window of 1024)
176 assert!(!state.is_duplicate(1500));
177 state.mark_received(1500);
178
179 // Now group 0 should be treated as very old and duplicate
180 assert!(state.is_duplicate(0));
181 }
182
183 #[test]
184 fn test_unsequenced_window_wrapping() {
185 let mut state = UnsequencedState::new();
186
187 // Start by receiving some packets to establish a base around 65000
188 for i in 0..3 {
189 let group = 65000 + i;
190 assert!(!state.is_duplicate(group));
191 state.mark_received(group);
192 }
193
194 // Now receive near the end of u16 range
195 assert!(!state.is_duplicate(65500));
196 state.mark_received(65500);
197
198 // Wrap around to 10 (should be treated as newer, after 65535)
199 assert!(!state.is_duplicate(10));
200 state.mark_received(10);
201
202 // Sending 65002 (old packet from before) should be duplicate
203 assert!(state.is_duplicate(65002));
204 }
205
206 #[test]
207 fn test_unsequenced_basic_duplicate_detection() {
208 let mut state = UnsequencedState::new();
209
210 // First packet should not be duplicate
211 assert!(!state.is_duplicate(5));
212 state.mark_received(5);
213
214 // Same packet again should be duplicate
215 assert!(state.is_duplicate(5));
216
217 // Different packet should not be duplicate
218 assert!(!state.is_duplicate(10));
219 state.mark_received(10);
220
221 // Both should now be marked
222 assert!(state.is_duplicate(5));
223 assert!(state.is_duplicate(10));
224 }
225
226 #[test]
227 fn test_unsequenced_window_boundaries() {
228 let mut state = UnsequencedState::new();
229
230 // Mark group 100 as base
231 state.mark_received(100);
232
233 // Within window (100 + 1023 = 1123)
234 assert!(!state.is_duplicate(1123));
235 state.mark_received(1123);
236 assert!(state.is_duplicate(1123));
237
238 // Just outside window should trigger slide
239 assert!(!state.is_duplicate(1124));
240 state.mark_received(1124);
241
242 // Old packet should now be duplicate
243 assert!(state.is_duplicate(100));
244 }
245
246 #[test]
247 fn test_next_outgoing_group() {
248 let mut state = UnsequencedState::new();
249
250 assert_eq!(state.next_outgoing_group(), 0);
251 assert_eq!(state.next_outgoing_group(), 1);
252 assert_eq!(state.next_outgoing_group(), 2);
253
254 // Test wrapping
255 state.outgoing_unsequenced_group = 65535;
256 assert_eq!(state.next_outgoing_group(), 65535);
257 assert_eq!(state.next_outgoing_group(), 0);
258 }
259
260 #[test]
261 fn test_getters() {
262 let mut state = UnsequencedState::new();
263
264 assert_eq!(state.incoming_group(), 0);
265 assert_eq!(state.outgoing_group(), 0);
266
267 state.mark_received(100);
268 assert_eq!(state.incoming_group(), 0); // Base doesn't move for small offsets
269
270 state.mark_received(1500); // Far ahead, should trigger slide
271 assert!(state.incoming_group() > 0); // Base should have moved
272
273 state.next_outgoing_group();
274 assert_eq!(state.outgoing_group(), 1);
275 }
276
277 #[test]
278 fn test_empty_window() {
279 let state = UnsequencedState::new();
280
281 // Empty window should not consider anything a duplicate
282 assert!(!state.is_duplicate(0));
283 assert!(!state.is_duplicate(100));
284 assert!(!state.is_duplicate(65535));
285 }
286
287 #[test]
288 fn test_complete_window_reset() {
289 let mut state = UnsequencedState::new();
290
291 // Mark some packets
292 state.mark_received(0);
293 state.mark_received(1);
294 state.mark_received(2);
295
296 // Jump very far ahead (more than 32 words)
297 state.mark_received(2000);
298
299 // Old packets should be duplicates (window was reset and moved)
300 assert!(state.is_duplicate(0));
301 assert!(state.is_duplicate(1));
302 assert!(state.is_duplicate(2));
303
304 // New packet in the new window should work
305 assert!(!state.is_duplicate(2001));
306 state.mark_received(2001);
307 assert!(state.is_duplicate(2001));
308 }
309}