Skip to main content

rns_core/resource/
receiver.rs

1use alloc::vec;
2use alloc::vec::Vec;
3
4use super::advertisement::ResourceAdvertisement;
5use super::assembly::{AssemblyData, AssemblyId, AssemblyResult, ResourceAssembly};
6use super::parts::map_hash;
7use super::types::*;
8use super::window::WindowState;
9use crate::buffer::types::Compressor;
10use crate::constants::*;
11
12/// Resource receiver state machine.
13///
14/// Unpacks advertisements, requests parts, receives parts, assembles data.
15/// Returns `Vec<ResourceAction>` — no I/O, no callbacks.
16pub struct ResourceReceiver {
17    assembly_id: Option<AssemblyId>,
18    /// Current status
19    pub status: ResourceStatus,
20    /// Resource hash (from advertisement, 32 bytes)
21    pub resource_hash: Vec<u8>,
22    /// Random hash (from advertisement)
23    pub random_hash: Vec<u8>,
24    /// Original hash
25    pub original_hash: Vec<u8>,
26    /// Flags
27    pub flags: AdvFlags,
28    /// Transfer size (encrypted)
29    pub transfer_size: u64,
30    /// Total uncompressed data size
31    pub data_size: u64,
32    /// Total parts
33    pub total_parts: usize,
34    /// Received parts data (None = not yet received)
35    parts: Vec<Option<Vec<u8>>>,
36    /// Hashmap: part index -> map_hash (None if not yet known)
37    hashmap: Vec<Option<[u8; RESOURCE_MAPHASH_LEN]>>,
38    /// Number of hashmap entries populated
39    hashmap_height: usize,
40    /// Whether we're waiting for a hashmap update
41    pub waiting_for_hmu: bool,
42    /// Number of parts received
43    pub received_count: usize,
44    /// Outstanding parts in current window request
45    pub outstanding_parts: usize,
46    /// Consecutive completed height (-1 means none)
47    consecutive_completed_height: isize,
48    /// SDU size
49    sdu: usize,
50    /// Link RTT estimate (from link establishment)
51    link_rtt: f64,
52    /// Retries left
53    pub retries_left: usize,
54    /// Max retries
55    max_retries: usize,
56    /// RTT estimate
57    pub rtt: Option<f64>,
58    /// Part timeout factor
59    part_timeout_factor: f64,
60    /// Last activity timestamp
61    pub last_activity: f64,
62    /// Request sent timestamp
63    pub req_sent: f64,
64    /// Request sent bytes
65    req_sent_bytes: usize,
66    /// Request response timestamp
67    req_resp: Option<f64>,
68    /// RTT received bytes
69    rtt_rxd_bytes: usize,
70    /// RTT received bytes at part request
71    rtt_rxd_bytes_at_part_req: usize,
72    /// Request response RTT rate
73    req_resp_rtt_rate: f64,
74    /// Request data RTT rate
75    req_data_rtt_rate: f64,
76    /// EIFR
77    pub eifr: Option<f64>,
78    /// Previous EIFR from prior transfer
79    previous_eifr: Option<f64>,
80    /// Segment index
81    pub segment_index: u64,
82    /// Total segments
83    pub total_segments: u64,
84    /// Has metadata
85    pub has_metadata: bool,
86    /// Request ID
87    pub request_id: Option<Vec<u8>>,
88    /// Window state
89    pub window: WindowState,
90    /// Original advertisement bytes for this incoming transfer.
91    pub advertisement_packet: Vec<u8>,
92    /// Maximum allowed decompressed size for compressed resource payloads.
93    pub max_decompressed_size: usize,
94}
95
96impl ResourceReceiver {
97    /// Create a receiver from an advertisement packet.
98    pub fn from_advertisement(
99        adv_data: &[u8],
100        sdu: usize,
101        link_rtt: f64,
102        now: f64,
103        previous_window: Option<usize>,
104        previous_eifr: Option<f64>,
105    ) -> Result<Self, ResourceError> {
106        let adv = ResourceAdvertisement::unpack(adv_data)?;
107
108        // Validate resource_hash is 32 bytes
109        if adv.resource_hash.len() != 32 {
110            return Err(ResourceError::InvalidAdvertisement);
111        }
112        if adv.random_hash.len() != RESOURCE_RANDOM_HASH_SIZE || adv.original_hash.len() != 32 {
113            return Err(ResourceError::InvalidAdvertisement);
114        }
115        // Split advertisements carry the logical size of the complete
116        // Resource in data_size, while each receiver only assembles one
117        // bounded segment. Aggregate receive policy is enforced by rns-net.
118        if adv.transfer_size > RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64
119            || (!adv.flags.split && adv.data_size > RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64)
120        {
121            return Err(ResourceError::TooLarge);
122        }
123
124        // Python derives the part count of the transfer it receives from the
125        // advertised (encrypted, compressed) transfer size of this segment:
126        // ceil(t / sdu). Senders compute `n` the same way over the same SDU,
127        // so a matching advertisement carries exactly that count. No tolerance
128        // applies: Python's receiver ignores `n` and expects exactly this many
129        // parts, so a transfer advertising any other count cannot complete.
130        let total_parts = usize::try_from(adv.num_parts).map_err(|_| ResourceError::TooLarge)?;
131        if total_parts == 0 {
132            return Err(ResourceError::InvalidAdvertisement);
133        }
134        let transfer_size =
135            usize::try_from(adv.transfer_size).map_err(|_| ResourceError::TooLarge)?;
136        let expected_parts = transfer_size.div_ceil(sdu.max(1));
137        if total_parts != expected_parts {
138            return Err(ResourceError::PartCountMismatch);
139        }
140
141        // Part storage grows as parts arrive, so an accepted advertisement
142        // that is never followed by data holds no per-part allocation. The
143        // hashmap holds only the entries the advertisement carries.
144        let initial_hashes = core::cmp::min(adv.hashmap.len() / RESOURCE_MAPHASH_LEN, total_parts);
145        let parts_vec: Vec<Option<Vec<u8>>> = Vec::new();
146        let mut hashmap_vec: Vec<Option<[u8; RESOURCE_MAPHASH_LEN]>> =
147            Vec::with_capacity(initial_hashes);
148
149        // Populate initial hashmap from advertisement
150        for chunk in adv
151            .hashmap
152            .chunks_exact(RESOURCE_MAPHASH_LEN)
153            .take(initial_hashes)
154        {
155            let mut h = [0u8; RESOURCE_MAPHASH_LEN];
156            h.copy_from_slice(chunk);
157            hashmap_vec.push(Some(h));
158        }
159        let hashmap_height = hashmap_vec.len();
160
161        let mut window_state = WindowState::new();
162        if let Some(prev_w) = previous_window {
163            window_state.restore(prev_w);
164        }
165
166        Ok(ResourceReceiver {
167            assembly_id: None,
168            status: ResourceStatus::None,
169            resource_hash: adv.resource_hash,
170            random_hash: adv.random_hash,
171            original_hash: adv.original_hash,
172            flags: adv.flags,
173            transfer_size: adv.transfer_size,
174            data_size: adv.data_size,
175            total_parts,
176            parts: parts_vec,
177            hashmap: hashmap_vec,
178            hashmap_height,
179            waiting_for_hmu: false,
180            received_count: 0,
181            outstanding_parts: 0,
182            consecutive_completed_height: -1,
183            sdu,
184            link_rtt,
185            retries_left: RESOURCE_MAX_RETRIES,
186            max_retries: RESOURCE_MAX_RETRIES,
187            rtt: None,
188            part_timeout_factor: RESOURCE_PART_TIMEOUT_FACTOR,
189            last_activity: now,
190            req_sent: 0.0,
191            req_sent_bytes: 0,
192            req_resp: None,
193            rtt_rxd_bytes: 0,
194            rtt_rxd_bytes_at_part_req: 0,
195            req_resp_rtt_rate: 0.0,
196            req_data_rtt_rate: 0.0,
197            eifr: None,
198            previous_eifr,
199            segment_index: adv.segment_index,
200            total_segments: adv.total_segments,
201            has_metadata: adv.flags.has_metadata,
202            request_id: adv.request_id,
203            window: window_state,
204            advertisement_packet: adv_data.to_vec(),
205            max_decompressed_size: RESOURCE_AUTO_COMPRESS_MAX_SIZE,
206        })
207    }
208
209    fn has_part(&self, index: usize) -> bool {
210        self.parts.get(index).is_some_and(Option::is_some)
211    }
212
213    fn store_part(&mut self, index: usize, data: Vec<u8>) {
214        if self.parts.len() <= index {
215            self.parts.resize(index + 1, None);
216        }
217        self.parts[index] = Some(data);
218    }
219
220    fn hash_at(&self, index: usize) -> Option<&[u8; RESOURCE_MAPHASH_LEN]> {
221        self.hashmap.get(index).and_then(Option::as_ref)
222    }
223
224    fn store_hash(&mut self, index: usize, hash: [u8; RESOURCE_MAPHASH_LEN]) {
225        if self.hashmap.len() <= index {
226            self.hashmap.resize(index + 1, None);
227        }
228        self.hashmap[index] = Some(hash);
229    }
230
231    /// Accept the advertised resource. Begins transfer.
232    pub fn accept(&mut self, now: f64) -> Vec<ResourceAction> {
233        if self.status >= ResourceStatus::Assembling {
234            return vec![];
235        }
236        self.status = ResourceStatus::Transferring;
237        self.last_activity = now;
238        self.request_next(now)
239    }
240
241    /// Reject the advertised resource.
242    pub fn reject(&mut self) -> Vec<ResourceAction> {
243        self.status = ResourceStatus::Rejected;
244        vec![ResourceAction::SendCancelReceiver(
245            self.resource_hash.clone(),
246        )]
247    }
248
249    /// Cancel an accepted incoming transfer.
250    pub fn cancel(&mut self) -> Vec<ResourceAction> {
251        if self.status < ResourceStatus::Complete {
252            self.status = ResourceStatus::Failed;
253            vec![ResourceAction::SendCancelReceiver(
254                self.resource_hash.clone(),
255            )]
256        } else {
257            Vec::new()
258        }
259    }
260
261    fn corrupt_actions(&mut self, error: ResourceError) -> Vec<ResourceAction> {
262        self.status = ResourceStatus::Corrupt;
263        vec![
264            ResourceAction::SendCancelReceiver(self.resource_hash.clone()),
265            ResourceAction::Failed(error),
266            ResourceAction::TeardownLink,
267        ]
268    }
269
270    /// Receive a part. Matches by map hash and stores it.
271    pub fn receive_part(&mut self, part_data: &[u8], now: f64) -> Vec<ResourceAction> {
272        if self.status >= ResourceStatus::Assembling {
273            return vec![];
274        }
275
276        self.last_activity = now;
277        self.retries_left = self.max_retries;
278
279        // Update RTT on first part of window
280        if self.req_resp.is_none() {
281            self.req_resp = Some(now);
282            let rtt = now - self.req_sent;
283            self.part_timeout_factor = RESOURCE_PART_TIMEOUT_FACTOR_AFTER_RTT;
284
285            if self.rtt.is_none() {
286                self.rtt = Some(rtt);
287            } else if let Some(current_rtt) = self.rtt {
288                if rtt < current_rtt {
289                    self.rtt = Some(f64::max(current_rtt - current_rtt * 0.05, rtt));
290                } else {
291                    self.rtt = Some(f64::min(current_rtt + current_rtt * 0.05, rtt));
292                }
293            }
294
295            if rtt > 0.0 {
296                let req_resp_cost = part_data.len() + self.req_sent_bytes;
297                self.req_resp_rtt_rate = req_resp_cost as f64 / rtt;
298                self.window.update_req_resp_rate(self.req_resp_rtt_rate);
299            }
300        }
301
302        self.status = ResourceStatus::Transferring;
303
304        // Compute map hash for this part
305        let part_hash = map_hash(part_data, &self.random_hash);
306
307        // Align with request_next(): the completed height itself is not part
308        // of the outstanding window, which starts at the following index.
309        let search_start = (self.consecutive_completed_height + 1) as usize;
310        let mut matched = false;
311        let search_end = core::cmp::min(search_start + self.window.window, self.total_parts);
312        for i in search_start..search_end {
313            if let Some(h) = self.hash_at(i) {
314                if *h == part_hash {
315                    if !self.has_part(i) {
316                        self.store_part(i, part_data.to_vec());
317                        self.rtt_rxd_bytes += part_data.len();
318                        self.received_count += 1;
319                        self.outstanding_parts = self.outstanding_parts.saturating_sub(1);
320
321                        // Walk forward to extend consecutive height
322                        let mut cp = (self.consecutive_completed_height + 1) as usize;
323                        while cp < self.total_parts && self.has_part(cp) {
324                            self.consecutive_completed_height = cp as isize;
325                            cp += 1;
326                        }
327
328                        matched = true;
329                    }
330                    break;
331                }
332            }
333        }
334
335        let mut actions = Vec::new();
336
337        // Check if all parts received
338        if self.received_count == self.total_parts {
339            actions.push(ResourceAction::ProgressUpdate {
340                received: self.received_count,
341                total: self.total_parts,
342            });
343            // Assembly will be triggered by caller
344            return actions;
345        }
346
347        if matched {
348            actions.push(ResourceAction::ProgressUpdate {
349                received: self.received_count,
350                total: self.total_parts,
351            });
352        }
353
354        // Request next window when outstanding is 0
355        if self.outstanding_parts == 0 && self.received_count < self.total_parts {
356            // Window complete — grow
357            self.window.on_window_complete();
358
359            // Update data rate
360            if self.req_sent > 0.0 {
361                let rtt = now - self.req_sent;
362                let req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req;
363                if rtt > 0.0 {
364                    self.req_data_rtt_rate = req_transferred as f64 / rtt;
365                    self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes;
366                    self.window.update_data_rate(self.req_data_rtt_rate);
367                }
368            }
369
370            let next_actions = self.request_next(now);
371            actions.extend(next_actions);
372        }
373
374        actions
375    }
376
377    /// Handle a hashmap update packet.
378    ///
379    /// HMU format: [resource_hash: 32 bytes][msgpack([segment, hashmap])]
380    pub fn handle_hashmap_update(&mut self, hmu_data: &[u8], now: f64) -> Vec<ResourceAction> {
381        if self.status >= ResourceStatus::Assembling || !self.waiting_for_hmu {
382            return vec![];
383        }
384
385        if hmu_data.len() < 32 || hmu_data[..32] != self.resource_hash {
386            return vec![];
387        }
388        if hmu_data.len() == 32 {
389            return self.cancel();
390        }
391
392        let payload = &hmu_data[32..];
393        let value = match crate::msgpack::unpack_exact(payload) {
394            Ok(v) => v,
395            Err(_) => return self.cancel(),
396        };
397
398        let arr = match value.as_array() {
399            Some(a) if a.len() == 2 => a,
400            _ => return self.cancel(),
401        };
402
403        let segment = match arr[0].as_uint() {
404            Some(s) => match usize::try_from(s) {
405                Ok(segment) => segment,
406                Err(_) => return self.cancel(),
407            },
408            None => return self.cancel(),
409        };
410
411        let hashmap_bytes = match arr[1].as_bin() {
412            Some(b) => b,
413            None => return self.cancel(),
414        };
415
416        if hashmap_bytes.is_empty() || !hashmap_bytes.len().is_multiple_of(RESOURCE_MAPHASH_LEN) {
417            return self.cancel();
418        }
419
420        let seg_len = RESOURCE_HASHMAP_MAX_LEN;
421        let segment_start = match segment.checked_mul(seg_len) {
422            Some(start) if start < self.total_parts => start,
423            _ => return self.cancel(),
424        };
425        let num_hashes = hashmap_bytes.len() / RESOURCE_MAPHASH_LEN;
426        if num_hashes > seg_len || segment_start.saturating_add(num_hashes) > self.total_parts {
427            return self.cancel();
428        }
429
430        self.last_activity = now;
431        self.retries_left = self.max_retries;
432        self.status = ResourceStatus::Transferring;
433
434        // Populate hashmap slots
435        for i in 0..num_hashes {
436            let idx = segment_start + i;
437            let start = i * RESOURCE_MAPHASH_LEN;
438            let end = start + RESOURCE_MAPHASH_LEN;
439            if self.hash_at(idx).is_none() {
440                self.hashmap_height += 1;
441            }
442            let mut h = [0u8; RESOURCE_MAPHASH_LEN];
443            h.copy_from_slice(&hashmap_bytes[start..end]);
444            self.store_hash(idx, h);
445        }
446
447        self.waiting_for_hmu = false;
448        self.request_next(now)
449    }
450
451    /// Build and return request for next window of parts.
452    pub fn request_next(&mut self, now: f64) -> Vec<ResourceAction> {
453        if self.status >= ResourceStatus::Assembling {
454            return vec![];
455        }
456        if self.status == ResourceStatus::Failed || self.waiting_for_hmu {
457            return vec![];
458        }
459
460        self.outstanding_parts = 0;
461        let mut hashmap_exhausted = RESOURCE_HASHMAP_IS_NOT_EXHAUSTED;
462        let mut requested_hashes = Vec::new();
463
464        let pn_start = (self.consecutive_completed_height + 1) as usize;
465        let search_end = core::cmp::min(pn_start + self.window.window, self.total_parts);
466        let mut i = 0;
467
468        for pn in pn_start..search_end {
469            if !self.has_part(pn) {
470                match self.hash_at(pn) {
471                    Some(h) => {
472                        requested_hashes.extend_from_slice(h);
473                        self.outstanding_parts += 1;
474                        i += 1;
475                    }
476                    None => {
477                        hashmap_exhausted = RESOURCE_HASHMAP_IS_EXHAUSTED;
478                    }
479                }
480            }
481            if i >= self.window.window || hashmap_exhausted == RESOURCE_HASHMAP_IS_EXHAUSTED {
482                break;
483            }
484        }
485
486        let mut request_data = Vec::new();
487        request_data.push(hashmap_exhausted);
488        if hashmap_exhausted == RESOURCE_HASHMAP_IS_EXHAUSTED {
489            // Append last known map hash
490            if self.hashmap_height > 0 {
491                if let Some(last_hash) = self.hash_at(self.hashmap_height - 1) {
492                    request_data.extend_from_slice(last_hash);
493                } else {
494                    request_data.extend_from_slice(&[0u8; RESOURCE_MAPHASH_LEN]);
495                }
496            } else {
497                request_data.extend_from_slice(&[0u8; RESOURCE_MAPHASH_LEN]);
498            }
499            self.waiting_for_hmu = true;
500        }
501
502        request_data.extend_from_slice(&self.resource_hash);
503        request_data.extend_from_slice(&requested_hashes);
504
505        self.last_activity = now;
506        self.req_sent = now;
507        self.req_sent_bytes = request_data.len();
508        self.req_resp = None;
509
510        vec![ResourceAction::SendRequest(request_data)]
511    }
512
513    /// Assemble received parts, decrypt, decompress, verify hash.
514    #[allow(clippy::type_complexity)]
515    pub fn assemble(
516        &mut self,
517        decrypt_fn: &dyn Fn(&[u8]) -> Result<Vec<u8>, ()>,
518        compressor: &dyn Compressor,
519    ) -> Vec<ResourceAction> {
520        match self.prepare_data(decrypt_fn) {
521            Ok(data) => self.finish_data(data.run(compressor)),
522            Err(actions) => actions,
523        }
524    }
525
526    /// Join and authenticate parts, then transfer owned plaintext to an assembly job.
527    /// Keep this receiver alive until completion or cancellation. A pending attempt
528    /// ignores duplicate parts and ordinary part-receive timeouts.
529    #[allow(clippy::type_complexity)]
530    pub fn prepare_assembly(
531        &mut self,
532        decrypt_fn: &dyn Fn(&[u8]) -> Result<Vec<u8>, ()>,
533    ) -> Result<ResourceAssembly, Vec<ResourceAction>> {
534        let data = self.prepare_data(decrypt_fn)?;
535        let id = AssemblyId::new();
536        self.assembly_id = Some(id.clone());
537        Ok(ResourceAssembly { id, data })
538    }
539
540    pub fn assembly_id(&self) -> Option<&AssemblyId> {
541        (self.status == ResourceStatus::Assembling)
542            .then_some(self.assembly_id.as_ref())
543            .flatten()
544    }
545
546    /// Publish a result only for the still-pending attempt that produced it.
547    /// Results from another receiver or a cancelled attempt are discarded.
548    pub fn complete_assembly(&mut self, result: AssemblyResult) -> Vec<ResourceAction> {
549        if self.assembly_id() != Some(&result.id) {
550            return Vec::new();
551        }
552        self.assembly_id = None;
553        self.finish_data(result.result)
554    }
555
556    /// Fail a pending worker attempt without blaming the peer for a local failure.
557    pub fn fail_assembly(&mut self, id: &AssemblyId) -> Vec<ResourceAction> {
558        if self.assembly_id() != Some(id) {
559            return Vec::new();
560        }
561        self.assembly_id = None;
562        let mut actions = self.cancel();
563        actions.push(ResourceAction::Failed(ResourceError::InvalidState));
564        actions
565    }
566
567    fn finish_data(
568        &mut self,
569        result: Result<Vec<ResourceAction>, ResourceError>,
570    ) -> Vec<ResourceAction> {
571        match result {
572            Ok(actions) => {
573                self.status = ResourceStatus::Complete;
574                actions
575            }
576            Err(error) => self.corrupt_actions(error),
577        }
578    }
579
580    #[allow(clippy::type_complexity)]
581    fn prepare_data(
582        &mut self,
583        decrypt_fn: &dyn Fn(&[u8]) -> Result<Vec<u8>, ()>,
584    ) -> Result<AssemblyData, Vec<ResourceAction>> {
585        if self.status >= ResourceStatus::Assembling || self.received_count != self.total_parts {
586            return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
587        }
588
589        self.status = ResourceStatus::Assembling;
590
591        // Part storage grows as parts arrive; a short vector means a part is missing.
592        if self.parts.len() != self.total_parts {
593            self.status = ResourceStatus::Failed;
594            return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
595        }
596
597        // Size from the parts actually held, not the advertised transfer size.
598        // Reserving once avoids geometric growth and copying the joined prefix.
599        let mut stream_len = 0usize;
600        for part in &self.parts {
601            match part {
602                Some(data) => match stream_len.checked_add(data.len()) {
603                    Some(len) => stream_len = len,
604                    None => {
605                        self.status = ResourceStatus::Failed;
606                        return Err(vec![ResourceAction::Failed(ResourceError::TooLarge)]);
607                    }
608                },
609                None => {
610                    self.status = ResourceStatus::Failed;
611                    return Err(vec![ResourceAction::Failed(ResourceError::InvalidState)]);
612                }
613            }
614        }
615        let stream = if self.parts.len() == 1 {
616            // Validation above guarantees this part exists. Reuse its allocation.
617            self.parts[0].take().unwrap()
618        } else {
619            let mut stream = Vec::with_capacity(stream_len);
620            for data in self.parts.iter().flatten() {
621                stream.extend_from_slice(data);
622            }
623            stream
624        };
625        // Assembly is terminal for part reception. Release the redundant parts
626        // before authentication allocates plaintext, including on failure.
627        self.parts = Vec::new();
628        self.hashmap = Vec::new();
629
630        // Decrypt
631        let decrypted = if self.flags.encrypted {
632            match decrypt_fn(&stream) {
633                Ok(d) => d,
634                Err(_) => {
635                    self.status = ResourceStatus::Failed;
636                    return Err(vec![ResourceAction::Failed(
637                        ResourceError::DecryptionFailed,
638                    )]);
639                }
640            }
641        } else {
642            stream
643        };
644
645        let (Ok(resource_hash), Ok(random_hash)) = (
646            self.resource_hash.as_slice().try_into(),
647            self.random_hash.as_slice().try_into(),
648        ) else {
649            return Err(self.corrupt_actions(ResourceError::InvalidAdvertisement));
650        };
651        Ok(AssemblyData {
652            plaintext: decrypted,
653            resource_hash,
654            random_hash,
655            compressed: self.flags.compressed,
656            metadata: self.has_metadata && self.segment_index == 1,
657            limit: self.max_decompressed_size,
658        })
659    }
660
661    /// Handle cancel from sender (RESOURCE_ICL).
662    pub fn handle_cancel(&mut self) -> Vec<ResourceAction> {
663        if self.status < ResourceStatus::Complete {
664            self.status = ResourceStatus::Failed;
665            return vec![ResourceAction::Failed(ResourceError::Rejected)];
666        }
667        vec![]
668    }
669
670    /// Periodic tick. Checks for timeouts.
671    #[allow(clippy::type_complexity)]
672    pub fn tick(
673        &mut self,
674        now: f64,
675        decrypt_fn: &dyn Fn(&[u8]) -> Result<Vec<u8>, ()>,
676        compressor: &dyn Compressor,
677    ) -> Vec<ResourceAction> {
678        if self.status >= ResourceStatus::Assembling {
679            return vec![];
680        }
681
682        if self.status == ResourceStatus::Transferring {
683            // Check if all parts received — trigger assembly
684            if self.received_count == self.total_parts {
685                return self.assemble(decrypt_fn, compressor);
686            }
687
688            // Compute timeout
689            let eifr = self.compute_eifr();
690            let retries_used = self.max_retries - self.retries_left;
691            let extra_wait = retries_used as f64 * RESOURCE_PER_RETRY_DELAY;
692            let expected_hmu_wait =
693                if eifr > 0.0 && (self.waiting_for_hmu || self.outstanding_parts == 0) {
694                    (self.sdu as f64 * 8.0 * RESOURCE_HMU_WAIT_FACTOR) / eifr
695                } else {
696                    0.0
697                };
698            let expected_tof = if self.outstanding_parts > 0 && eifr > 0.0 {
699                (self.outstanding_parts as f64 * self.sdu as f64 * 8.0) / eifr
700            } else if eifr > 0.0 {
701                (3.0 * self.sdu as f64) / eifr
702            } else {
703                10.0 // fallback
704            };
705
706            let sleep_time = self.last_activity
707                + self.part_timeout_factor * expected_tof
708                + expected_hmu_wait
709                + RESOURCE_RETRY_GRACE_TIME
710                + extra_wait;
711
712            if now > sleep_time {
713                if self.retries_left > 0 {
714                    // Timeout — shrink window, retry
715                    self.window.on_timeout();
716                    self.retries_left -= 1;
717                    self.waiting_for_hmu = false;
718                    return self.request_next(now);
719                } else {
720                    self.status = ResourceStatus::Failed;
721                    return vec![ResourceAction::Failed(ResourceError::MaxRetriesExceeded)];
722                }
723            }
724        }
725
726        vec![]
727    }
728
729    /// Compute EIFR (expected inflight rate) and update self.eifr.
730    fn compute_eifr(&mut self) -> f64 {
731        let eifr = if self.req_data_rtt_rate > 0.0 {
732            self.req_data_rtt_rate * 8.0
733        } else if let Some(prev) = self.previous_eifr {
734            prev
735        } else {
736            // Fallback: use link_rtt as establishment cost estimate
737            let rtt = self.rtt.unwrap_or(self.link_rtt);
738            if rtt > 0.0 {
739                (self.sdu as f64 * 8.0) / rtt
740            } else {
741                10000.0
742            }
743        };
744        self.eifr = Some(eifr);
745        eifr
746    }
747
748    /// Get current progress as (received, total).
749    pub fn progress(&self) -> (usize, usize) {
750        (self.received_count, self.total_parts)
751    }
752
753    /// Get window and EIFR for passing to next transfer.
754    pub fn get_transfer_state(&self) -> (usize, Option<f64>) {
755        (self.window.window, self.eifr)
756    }
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762    use crate::buffer::types::{Compressor, DecompressError, NoopCompressor};
763    use crate::resource::advertisement::ResourceAdvertisement;
764    use crate::resource::sender::ResourceSender;
765
766    fn identity_encrypt(data: &[u8]) -> Vec<u8> {
767        data.to_vec()
768    }
769
770    fn identity_decrypt(data: &[u8]) -> Result<Vec<u8>, ()> {
771        Ok(data.to_vec())
772    }
773
774    struct ExpandingCompressor;
775
776    impl Compressor for ExpandingCompressor {
777        fn compress(&self, data: &[u8]) -> Option<Vec<u8>> {
778            Some(data[..data.len() / 2].to_vec())
779        }
780
781        fn decompress(&self, data: &[u8]) -> Option<Vec<u8>> {
782            self.decompress_bounded(data, usize::MAX).ok()
783        }
784
785        fn decompress_bounded(
786            &self,
787            data: &[u8],
788            max_output_size: usize,
789        ) -> Result<Vec<u8>, DecompressError> {
790            let mut out = data.to_vec();
791            out.extend_from_slice(data);
792            if out.len() > max_output_size {
793                return Err(DecompressError::TooLarge);
794            }
795            Ok(out)
796        }
797    }
798
799    fn base_timeout(receiver: &ResourceReceiver, eifr: f64) -> f64 {
800        let expected_tof = if receiver.outstanding_parts > 0 {
801            (receiver.outstanding_parts as f64 * receiver.sdu as f64 * 8.0) / eifr
802        } else {
803            (3.0 * receiver.sdu as f64) / eifr
804        };
805
806        receiver.last_activity
807            + receiver.part_timeout_factor * expected_tof
808            + RESOURCE_RETRY_GRACE_TIME
809    }
810
811    fn hmu_timeout(receiver: &ResourceReceiver, eifr: f64) -> f64 {
812        let expected_hmu_wait = (receiver.sdu as f64 * 8.0 * RESOURCE_HMU_WAIT_FACTOR) / eifr;
813        base_timeout(receiver, eifr) + expected_hmu_wait
814    }
815
816    fn make_sender_receiver() -> (ResourceSender, ResourceReceiver) {
817        let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
818        let data = b"Hello, Resource Transfer!";
819
820        let sender = ResourceSender::new(
821            data,
822            None,
823            RESOURCE_SDU,
824            &identity_encrypt,
825            &NoopCompressor,
826            &mut rng,
827            1000.0,
828            false,
829            false,
830            None,
831            1,
832            1,
833            None,
834            0.5,
835            6.0,
836        )
837        .unwrap();
838
839        let adv_data = sender.get_advertisement(0);
840        let receiver =
841            ResourceReceiver::from_advertisement(&adv_data, RESOURCE_SDU, 0.5, 1000.0, None, None)
842                .unwrap();
843
844        (sender, receiver)
845    }
846
847    fn ready_receiver() -> ResourceReceiver {
848        let (mut sender, mut receiver) = make_sender_receiver();
849        for action in receiver.accept(1001.0) {
850            if let ResourceAction::SendRequest(request) = action {
851                for action in sender.handle_request(&request, 1002.0) {
852                    if let ResourceAction::SendPart(part) = action {
853                        receiver.receive_part(&part, 1003.0);
854                    }
855                }
856            }
857        }
858        assert_eq!(receiver.received_count, receiver.total_parts);
859        receiver
860    }
861
862    #[test]
863    fn assembly_handoff_matches_synchronous_actions_and_releases_parts() {
864        let mut receiver = ready_receiver();
865        let job = receiver.prepare_assembly(&identity_decrypt).unwrap();
866        assert!(receiver.parts.is_empty() && receiver.hashmap.is_empty());
867        receiver.waiting_for_hmu = true;
868        assert!(receiver
869            .handle_hashmap_update(&receiver.resource_hash.clone(), 1004.0)
870            .is_empty());
871        assert!(receiver.accept(1004.0).is_empty());
872        assert!(receiver.request_next(1004.0).is_empty());
873        assert_eq!(receiver.assembly_id(), Some(job.id()));
874        assert!(receiver.receive_part(b"duplicate", 1004.0).is_empty());
875        assert!(receiver
876            .tick(
877                100000.0,
878                &|_| panic!("pending assembly decrypted again"),
879                &NoopCompressor
880            )
881            .is_empty());
882        assert!(receiver.prepare_assembly(&identity_decrypt).is_err());
883        let actions = receiver.complete_assembly(job.run(&NoopCompressor));
884        let expected = ready_receiver().assemble(&identity_decrypt, &NoopCompressor);
885        assert_eq!(format!("{actions:?}"), format!("{expected:?}"));
886        assert_eq!(receiver.status, ResourceStatus::Complete);
887        assert!(receiver.assembly_id().is_none());
888    }
889
890    #[test]
891    fn assembly_result_requires_original_live_attempt_even_for_same_hash() {
892        let mut first = ready_receiver();
893        let mut second = ready_receiver();
894        assert_eq!(first.resource_hash, second.resource_hash);
895        let first_job = first.prepare_assembly(&identity_decrypt).unwrap();
896        let second_job = second.prepare_assembly(&identity_decrypt).unwrap();
897        assert_ne!(first_job.id(), second_job.id());
898        assert!(first
899            .complete_assembly(second_job.run(&NoopCompressor))
900            .is_empty());
901        first.cancel();
902        assert!(first
903            .complete_assembly(first_job.run(&NoopCompressor))
904            .is_empty());
905        assert_eq!(first.status, ResourceStatus::Failed);
906    }
907
908    #[test]
909    fn assembly_local_failure_and_authentication_failure_cannot_publish_success() {
910        let mut receiver = ready_receiver();
911        assert!(receiver.prepare_assembly(&|_| Err(())).is_err());
912        assert!(receiver.assembly_id().is_none());
913        assert!(receiver.parts.is_empty() && receiver.hashmap.is_empty());
914        assert!(receiver
915            .prepare_assembly(&|_| panic!("failed assembly retried"))
916            .is_err());
917        let mut receiver = ready_receiver();
918        let job = receiver.prepare_assembly(&identity_decrypt).unwrap();
919        let actions = receiver.fail_assembly(job.id());
920        assert!(matches!(
921            actions.as_slice(),
922            [
923                ResourceAction::SendCancelReceiver(_),
924                ResourceAction::Failed(ResourceError::InvalidState)
925            ]
926        ));
927        assert!(receiver.fail_assembly(job.id()).is_empty());
928        assert!(receiver
929            .complete_assembly(job.run(&NoopCompressor))
930            .is_empty());
931    }
932
933    #[test]
934    fn test_assembly_joins_actual_parts_independent_of_advertised_size() {
935        let (mut sender, mut receiver) = make_sender_receiver();
936        let mut stream = Vec::new();
937        for action in receiver.accept(1001.0) {
938            if let ResourceAction::SendRequest(request) = action {
939                for response in sender.handle_request(&request, 1002.0) {
940                    if let ResourceAction::SendPart(part) = response {
941                        stream.extend_from_slice(&part);
942                    }
943                }
944            }
945        }
946        // Exercise uneven parts and a misleading size without allocating from it.
947        receiver.parts = stream.chunks(7).map(|part| Some(part.to_vec())).collect();
948        receiver.total_parts = receiver.parts.len();
949        receiver.received_count = receiver.total_parts;
950        receiver.transfer_size = u64::MAX;
951        let actions = receiver.assemble(
952            &|joined| {
953                assert_eq!(joined, stream);
954                identity_decrypt(joined)
955            },
956            &NoopCompressor,
957        );
958        assert!(actions.iter().any(|action| matches!(action,
959            ResourceAction::DataReceived { data, .. } if data == b"Hello, Resource Transfer!")));
960        assert_eq!(receiver.status, ResourceStatus::Complete);
961    }
962
963    #[test]
964    fn test_assembly_rejects_missing_part_before_decryption() {
965        let (_, mut receiver) = make_sender_receiver();
966        receiver.received_count = receiver.total_parts;
967        let actions = receiver.assemble(
968            &|_| panic!("missing part reached decryption"),
969            &NoopCompressor,
970        );
971        assert!(matches!(
972            actions.as_slice(),
973            [ResourceAction::Failed(ResourceError::InvalidState)]
974        ));
975        assert_eq!(receiver.status, ResourceStatus::Failed);
976    }
977
978    #[test]
979    fn test_from_advertisement() {
980        let (sender, receiver) = make_sender_receiver();
981        assert_eq!(receiver.total_parts, sender.total_parts());
982        assert_eq!(receiver.transfer_size, sender.transfer_size as u64);
983        assert_eq!(receiver.resource_hash, sender.resource_hash.to_vec());
984        assert!(!receiver.advertisement_packet.is_empty());
985        assert_eq!(
986            receiver.max_decompressed_size,
987            RESOURCE_AUTO_COMPRESS_MAX_SIZE
988        );
989    }
990
991    #[test]
992    fn test_from_advertisement_rejects_huge_part_count() {
993        let adv = ResourceAdvertisement {
994            transfer_size: 1024,
995            data_size: 1024,
996            num_parts: u64::MAX,
997            resource_hash: vec![0x11; 32],
998            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
999            original_hash: vec![0x33; 32],
1000            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN],
1001            flags: AdvFlags {
1002                encrypted: false,
1003                compressed: false,
1004                split: false,
1005                is_request: false,
1006                is_response: false,
1007                has_metadata: false,
1008            },
1009            segment_index: 1,
1010            total_segments: 1,
1011            request_id: None,
1012        };
1013
1014        let err = match ResourceReceiver::from_advertisement(
1015            &adv.pack(0),
1016            RESOURCE_SDU,
1017            0.5,
1018            1000.0,
1019            None,
1020            None,
1021        ) {
1022            Ok(_) => panic!("huge advertisement should be rejected"),
1023            Err(err) => err,
1024        };
1025
1026        assert_eq!(err, ResourceError::PartCountMismatch);
1027    }
1028
1029    fn sized_advertisement(transfer_size: u64, num_parts: u64) -> ResourceAdvertisement {
1030        ResourceAdvertisement {
1031            transfer_size,
1032            data_size: transfer_size,
1033            num_parts,
1034            resource_hash: vec![0x11; 32],
1035            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
1036            original_hash: vec![0x33; 32],
1037            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN * num_parts.min(512) as usize],
1038            flags: AdvFlags {
1039                encrypted: true,
1040                compressed: false,
1041                split: false,
1042                is_request: false,
1043                is_response: false,
1044                has_metadata: false,
1045            },
1046            segment_index: 1,
1047            total_segments: 1,
1048            request_id: None,
1049        }
1050    }
1051
1052    fn receive_advertisement(
1053        adv: &ResourceAdvertisement,
1054        sdu: usize,
1055    ) -> Result<ResourceReceiver, ResourceError> {
1056        ResourceReceiver::from_advertisement(&adv.pack(0), sdu, 0.5, 1000.0, None, None)
1057    }
1058
1059    #[test]
1060    fn part_count_above_transfer_size_is_refused() {
1061        let adv = sized_advertisement(1, 140_000);
1062        assert_eq!(
1063            receive_advertisement(&adv, RESOURCE_SDU).err(),
1064            Some(ResourceError::PartCountMismatch)
1065        );
1066        assert_eq!(
1067            receive_advertisement(&adv, 1).err(),
1068            Some(ResourceError::PartCountMismatch)
1069        );
1070    }
1071
1072    #[test]
1073    fn part_count_below_transfer_size_is_refused() {
1074        let sdu = RESOURCE_SDU;
1075        let adv = sized_advertisement(10 * sdu as u64 + 1, 10);
1076        assert_eq!(
1077            receive_advertisement(&adv, sdu).err(),
1078            Some(ResourceError::PartCountMismatch)
1079        );
1080    }
1081
1082    #[test]
1083    fn part_count_matching_transfer_size_is_accepted() {
1084        let sdu = RESOURCE_SDU;
1085        let receiver = receive_advertisement(&sized_advertisement(1, 1), sdu).unwrap();
1086        assert_eq!(receiver.total_parts, 1);
1087
1088        let receiver = receive_advertisement(&sized_advertisement(sdu as u64 + 1, 2), sdu).unwrap();
1089        assert_eq!(receiver.total_parts, 2);
1090    }
1091
1092    #[test]
1093    fn transfer_size_on_sdu_boundary_needs_no_extra_part() {
1094        let sdu = RESOURCE_SDU;
1095        let exact = 3 * sdu as u64;
1096        let receiver = receive_advertisement(&sized_advertisement(exact, 3), sdu).unwrap();
1097        assert_eq!(receiver.total_parts, 3);
1098        assert_eq!(
1099            receive_advertisement(&sized_advertisement(exact, 4), sdu).err(),
1100            Some(ResourceError::PartCountMismatch)
1101        );
1102        assert_eq!(
1103            receive_advertisement(&sized_advertisement(exact, 2), sdu).err(),
1104            Some(ResourceError::PartCountMismatch)
1105        );
1106    }
1107
1108    #[test]
1109    fn split_segment_part_count_follows_segment_transfer_size() {
1110        let sdu = RESOURCE_SDU;
1111        let segment_size = RESOURCE_MAX_EFFICIENT_SIZE as u64 + 61;
1112        let segment_parts = segment_size.div_ceil(sdu as u64);
1113        let mut adv = sized_advertisement(segment_size, segment_parts);
1114        adv.data_size = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1115        adv.flags.split = true;
1116        adv.segment_index = 2;
1117        adv.total_segments = 3;
1118
1119        let receiver = receive_advertisement(&adv, sdu).unwrap();
1120        assert_eq!(receiver.total_parts as u64, segment_parts);
1121        assert_eq!(receiver.segment_index, 2);
1122
1123        // The logical size of the whole split resource does not set the count.
1124        adv.num_parts = adv.data_size.div_ceil(sdu as u64);
1125        assert_eq!(
1126            receive_advertisement(&adv, sdu).err(),
1127            Some(ResourceError::PartCountMismatch)
1128        );
1129    }
1130
1131    #[test]
1132    fn transfer_size_above_segment_limit_is_refused_up_front() {
1133        let limit = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1134        let adv = sized_advertisement(limit + 1, (limit + 1).div_ceil(RESOURCE_SDU as u64));
1135        assert_eq!(
1136            receive_advertisement(&adv, RESOURCE_SDU).err(),
1137            Some(ResourceError::InvalidAdvertisement)
1138        );
1139    }
1140
1141    #[test]
1142    fn idle_accepted_advertisement_holds_no_part_storage() {
1143        let sdu = 48;
1144        let limit = 3 * RESOURCE_MAX_EFFICIENT_SIZE as u64;
1145        let parts = limit.div_ceil(sdu as u64);
1146        let mut receiver = receive_advertisement(&sized_advertisement(limit, parts), sdu).unwrap();
1147        assert_eq!(receiver.total_parts as u64, parts);
1148
1149        let actions = receiver.accept(1000.0);
1150        assert!(actions
1151            .iter()
1152            .any(|action| matches!(action, ResourceAction::SendRequest(_))));
1153        assert_eq!(receiver.parts.capacity(), 0);
1154        assert!(receiver.hashmap.capacity() <= RESOURCE_HASHMAP_MAX_LEN);
1155        assert_eq!(receiver.hashmap.len(), receiver.hashmap_height);
1156    }
1157
1158    #[test]
1159    fn part_storage_grows_with_received_parts() {
1160        let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
1161        let sdu = 8;
1162        let data: Vec<u8> = (0..100u8).collect();
1163        let mut sender = ResourceSender::new(
1164            &data,
1165            None,
1166            sdu,
1167            &identity_encrypt,
1168            &NoopCompressor,
1169            &mut rng,
1170            1000.0,
1171            false,
1172            false,
1173            None,
1174            1,
1175            1,
1176            None,
1177            0.5,
1178            6.0,
1179        )
1180        .unwrap();
1181        let mut receiver = ResourceReceiver::from_advertisement(
1182            &sender.get_advertisement(0),
1183            sdu,
1184            0.5,
1185            1000.0,
1186            None,
1187            None,
1188        )
1189        .unwrap();
1190        assert!(receiver.total_parts > 1);
1191        assert!(receiver.parts.is_empty());
1192
1193        let request = receiver
1194            .accept(1000.0)
1195            .into_iter()
1196            .find_map(|action| match action {
1197                ResourceAction::SendRequest(request) => Some(request),
1198                _ => None,
1199            })
1200            .unwrap();
1201        let first_part = sender
1202            .handle_request(&request, 1001.0)
1203            .into_iter()
1204            .find_map(|action| match action {
1205                ResourceAction::SendPart(part) => Some(part),
1206                _ => None,
1207            })
1208            .unwrap();
1209        receiver.receive_part(&first_part, 1002.0);
1210        assert_eq!(receiver.received_count, 1);
1211        assert_eq!(receiver.parts.len(), 1);
1212    }
1213
1214    #[test]
1215    fn test_from_advertisement_rejects_oversized_transfer() {
1216        let adv = ResourceAdvertisement {
1217            transfer_size: RESOURCE_AUTO_COMPRESS_MAX_SIZE as u64 + 1,
1218            data_size: 1024,
1219            num_parts: 1,
1220            resource_hash: vec![0x11; 32],
1221            random_hash: vec![0x22; RESOURCE_RANDOM_HASH_SIZE],
1222            original_hash: vec![0x33; 32],
1223            hashmap: vec![0x44; RESOURCE_MAPHASH_LEN],
1224            flags: AdvFlags {
1225                encrypted: false,
1226                compressed: false,
1227                split: false,
1228                is_request: false,
1229                is_response: false,
1230                has_metadata: false,
1231            },
1232            segment_index: 1,
1233            total_segments: 1,
1234            request_id: None,
1235        };
1236
1237        let err = match ResourceReceiver::from_advertisement(
1238            &adv.pack(0),
1239            RESOURCE_SDU,
1240            0.5,
1241            1000.0,
1242            None,
1243            None,
1244        ) {
1245            Ok(_) => panic!("oversized advertisement should be rejected"),
1246            Err(err) => err,
1247        };
1248
1249        assert_eq!(err, ResourceError::InvalidAdvertisement);
1250    }
1251
1252    #[test]
1253    fn test_accept() {
1254        let (_, mut receiver) = make_sender_receiver();
1255        let actions = receiver.accept(1000.0);
1256        assert_eq!(receiver.status, ResourceStatus::Transferring);
1257        assert!(!actions.is_empty());
1258        assert!(actions
1259            .iter()
1260            .any(|a| matches!(a, ResourceAction::SendRequest(_))));
1261    }
1262
1263    #[test]
1264    fn test_reject() {
1265        let (_, mut receiver) = make_sender_receiver();
1266        let actions = receiver.reject();
1267        assert_eq!(receiver.status, ResourceStatus::Rejected);
1268        assert!(actions
1269            .iter()
1270            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1271    }
1272
1273    #[test]
1274    fn test_cancel_is_distinct_from_advertisement_rejection() {
1275        let (_, mut receiver) = make_sender_receiver();
1276        receiver.accept(1000.0);
1277        let actions = receiver.cancel();
1278        assert_eq!(receiver.status, ResourceStatus::Failed);
1279        assert!(actions
1280            .iter()
1281            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1282        assert!(receiver.cancel().is_empty());
1283    }
1284
1285    fn hmu(resource_hash: &[u8], segment: u64, hashmap: Vec<u8>) -> Vec<u8> {
1286        let payload = crate::msgpack::pack(&crate::msgpack::Value::Array(vec![
1287            crate::msgpack::Value::UInt(segment),
1288            crate::msgpack::Value::Bin(hashmap),
1289        ]));
1290        let mut data = resource_hash.to_vec();
1291        data.extend_from_slice(&payload);
1292        data
1293    }
1294
1295    #[test]
1296    fn test_unsolicited_hmu_is_ignored_without_refreshing_timeout_state() {
1297        let (_, mut receiver) = make_sender_receiver();
1298        receiver.status = ResourceStatus::Transferring;
1299        receiver.waiting_for_hmu = false;
1300        receiver.last_activity = 1000.0;
1301        receiver.retries_left = 1;
1302        let update = hmu(&receiver.resource_hash, 0, vec![0x99; RESOURCE_MAPHASH_LEN]);
1303
1304        let actions = receiver.handle_hashmap_update(&update, 2000.0);
1305
1306        assert!(actions.is_empty());
1307        assert_eq!(receiver.status, ResourceStatus::Transferring);
1308        assert!(!receiver.waiting_for_hmu);
1309        assert_eq!(receiver.last_activity, 1000.0);
1310        assert_eq!(receiver.retries_left, 1);
1311    }
1312
1313    #[test]
1314    fn test_empty_hmu_cancels_waiting_receiver() {
1315        let (_, mut receiver) = make_sender_receiver();
1316        receiver.status = ResourceStatus::Transferring;
1317        receiver.waiting_for_hmu = true;
1318        let update = hmu(&receiver.resource_hash, 1, Vec::new());
1319
1320        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1321
1322        assert_eq!(receiver.status, ResourceStatus::Failed);
1323        assert!(actions
1324            .iter()
1325            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1326        assert!(!actions
1327            .iter()
1328            .any(|action| matches!(action, ResourceAction::SendRequest(_))));
1329    }
1330
1331    #[test]
1332    fn test_malformed_hmu_cancels_waiting_receiver() {
1333        let (_, mut receiver) = make_sender_receiver();
1334        receiver.status = ResourceStatus::Transferring;
1335        receiver.waiting_for_hmu = true;
1336        let mut malformed = receiver.resource_hash.clone();
1337        malformed.extend_from_slice(&[0xc1]);
1338
1339        let actions = receiver.handle_hashmap_update(&malformed, 1001.0);
1340
1341        assert_eq!(receiver.status, ResourceStatus::Failed);
1342        assert!(actions
1343            .iter()
1344            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1345    }
1346
1347    #[test]
1348    fn test_hmu_without_payload_cancels_waiting_receiver() {
1349        let (_, mut receiver) = make_sender_receiver();
1350        receiver.status = ResourceStatus::Transferring;
1351        receiver.waiting_for_hmu = true;
1352        let update = receiver.resource_hash.clone();
1353
1354        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1355
1356        assert_eq!(receiver.status, ResourceStatus::Failed);
1357        assert!(actions
1358            .iter()
1359            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1360    }
1361
1362    #[test]
1363    fn test_hmu_for_another_resource_is_ignored() {
1364        let (_, mut receiver) = make_sender_receiver();
1365        receiver.status = ResourceStatus::Transferring;
1366        receiver.waiting_for_hmu = true;
1367        receiver.last_activity = 1000.0;
1368        let update = hmu(&[0xEE; 32], 0, vec![0x99; RESOURCE_MAPHASH_LEN]);
1369
1370        let actions = receiver.handle_hashmap_update(&update, 2000.0);
1371
1372        assert!(actions.is_empty());
1373        assert!(receiver.waiting_for_hmu);
1374        assert_eq!(receiver.last_activity, 1000.0);
1375    }
1376
1377    #[test]
1378    fn test_misaligned_hmu_hashmap_cancels_waiting_receiver() {
1379        let (_, mut receiver) = make_sender_receiver();
1380        receiver.status = ResourceStatus::Transferring;
1381        receiver.waiting_for_hmu = true;
1382        let update = hmu(
1383            &receiver.resource_hash,
1384            0,
1385            vec![0x99; RESOURCE_MAPHASH_LEN + 1],
1386        );
1387
1388        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1389
1390        assert_eq!(receiver.status, ResourceStatus::Failed);
1391        assert!(actions
1392            .iter()
1393            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1394    }
1395
1396    #[test]
1397    fn test_out_of_range_hmu_segment_cancels_waiting_receiver() {
1398        let (_, mut receiver) = make_sender_receiver();
1399        receiver.status = ResourceStatus::Transferring;
1400        receiver.waiting_for_hmu = true;
1401        let update = hmu(
1402            &receiver.resource_hash,
1403            u64::MAX,
1404            vec![0x99; RESOURCE_MAPHASH_LEN],
1405        );
1406
1407        let actions = receiver.handle_hashmap_update(&update, 1001.0);
1408
1409        assert_eq!(receiver.status, ResourceStatus::Failed);
1410        assert!(actions
1411            .iter()
1412            .any(|action| matches!(action, ResourceAction::SendCancelReceiver(_))));
1413    }
1414
1415    #[test]
1416    fn test_tick_is_inert_once_receiver_starts_assembling() {
1417        for status in [
1418            ResourceStatus::Assembling,
1419            ResourceStatus::Complete,
1420            ResourceStatus::Failed,
1421            ResourceStatus::Corrupt,
1422            ResourceStatus::Rejected,
1423        ] {
1424            let (_, mut receiver) = make_sender_receiver();
1425            receiver.status = status;
1426            receiver.last_activity = 1.0;
1427            receiver.retries_left = 0;
1428
1429            let actions = receiver.tick(10_000.0, &identity_decrypt, &NoopCompressor);
1430
1431            assert!(actions.is_empty(), "tick emitted an action for {status:?}");
1432            assert_eq!(receiver.status, status);
1433            assert_eq!(receiver.last_activity, 1.0);
1434        }
1435    }
1436
1437    #[test]
1438    fn test_receive_part_stores() {
1439        let (mut sender, mut receiver) = make_sender_receiver();
1440        receiver.accept(1000.0);
1441
1442        // Get part data from sender (we use identity encryption so parts ARE the raw data)
1443        // Request first part
1444        let mut request = Vec::new();
1445        request.push(RESOURCE_HASHMAP_IS_NOT_EXHAUSTED);
1446        request.extend_from_slice(&sender.resource_hash);
1447        request.extend_from_slice(&sender.part_hashes[0]);
1448
1449        let send_actions = sender.handle_request(&request, 1001.0);
1450        let part_data = send_actions
1451            .iter()
1452            .find_map(|a| match a {
1453                ResourceAction::SendPart(d) => Some(d.clone()),
1454                _ => None,
1455            })
1456            .unwrap();
1457
1458        // Give it to receiver
1459        receiver.req_sent = 1000.5;
1460        let _actions = receiver.receive_part(&part_data, 1001.0);
1461        assert_eq!(receiver.received_count, 1);
1462    }
1463
1464    #[test]
1465    fn test_consecutive_completed_height() {
1466        let (sender, mut receiver) = make_sender_receiver();
1467        receiver.accept(1000.0);
1468
1469        // Simulate receiving parts in order for a multi-part resource
1470        if sender.total_parts() > 1 {
1471            // This only applies to multi-part transfers
1472            assert_eq!(receiver.consecutive_completed_height, -1);
1473        }
1474    }
1475
1476    #[test]
1477    fn receive_part_search_window_matches_request_after_completed_height() {
1478        let mut rng = rns_crypto::FixedRng::new(&[0x5a; 64]);
1479        let data: Vec<u8> = (0..48).collect();
1480        let mut sender = ResourceSender::new(
1481            &data,
1482            None,
1483            8,
1484            &identity_encrypt,
1485            &NoopCompressor,
1486            &mut rng,
1487            1000.0,
1488            false,
1489            false,
1490            None,
1491            1,
1492            1,
1493            None,
1494            0.5,
1495            6.0,
1496        )
1497        .unwrap();
1498        assert!(sender.total_parts() > 5);
1499
1500        let advertisement = sender.get_advertisement(0);
1501        let mut receiver =
1502            ResourceReceiver::from_advertisement(&advertisement, 8, 0.5, 1000.0, None, None)
1503                .unwrap();
1504        let first_request = receiver
1505            .accept(1000.0)
1506            .into_iter()
1507            .find_map(|action| match action {
1508                ResourceAction::SendRequest(data) => Some(data),
1509                _ => None,
1510            })
1511            .unwrap();
1512        let first_parts = sender.handle_request(&first_request, 1001.0);
1513        let first_part = first_parts
1514            .iter()
1515            .find_map(|action| match action {
1516                ResourceAction::SendPart(data)
1517                    if map_hash(data, &receiver.random_hash) == sender.part_hashes[0] =>
1518                {
1519                    Some(data.clone())
1520                }
1521                _ => None,
1522            })
1523            .unwrap();
1524        receiver.receive_part(&first_part, 1002.0);
1525        assert_eq!(receiver.consecutive_completed_height, 0);
1526
1527        let next_request = receiver
1528            .request_next(1003.0)
1529            .into_iter()
1530            .find_map(|action| match action {
1531                ResourceAction::SendRequest(data) => Some(data),
1532                _ => None,
1533            })
1534            .unwrap();
1535        let next_parts = sender.handle_request(&next_request, 1004.0);
1536        let window_tail_index = receiver.window.window;
1537        let window_tail = next_parts
1538            .iter()
1539            .find_map(|action| match action {
1540                ResourceAction::SendPart(data)
1541                    if map_hash(data, &receiver.random_hash)
1542                        == sender.part_hashes[window_tail_index] =>
1543                {
1544                    Some(data.clone())
1545                }
1546                _ => None,
1547            })
1548            .expect("request must include the inclusive tail of the next window");
1549
1550        let actions = receiver.receive_part(&window_tail, 1005.0);
1551        assert!(receiver.parts[window_tail_index].is_some());
1552        assert_eq!(receiver.received_count, 2);
1553        assert_eq!(receiver.consecutive_completed_height, 0);
1554        assert!(actions.iter().any(|action| matches!(
1555            action,
1556            ResourceAction::ProgressUpdate {
1557                received: 2,
1558                total,
1559            } if *total == receiver.total_parts
1560        )));
1561    }
1562
1563    #[test]
1564    fn test_handle_cancel() {
1565        let (_, mut receiver) = make_sender_receiver();
1566        receiver.accept(1000.0);
1567        let _actions = receiver.handle_cancel();
1568        assert_eq!(receiver.status, ResourceStatus::Failed);
1569    }
1570
1571    #[test]
1572    fn test_assemble_compressed_resource_rejects_oversized_decompression() {
1573        let data = b"oversized!";
1574        let mut rng = rns_crypto::FixedRng::new(&[0x93; 64]);
1575
1576        let mut sender = ResourceSender::new(
1577            data,
1578            None,
1579            RESOURCE_SDU,
1580            &identity_encrypt,
1581            &ExpandingCompressor,
1582            &mut rng,
1583            1000.0,
1584            true,
1585            false,
1586            None,
1587            1,
1588            1,
1589            None,
1590            0.5,
1591            6.0,
1592        )
1593        .unwrap();
1594
1595        let adv = sender.get_advertisement(0);
1596        let mut receiver =
1597            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
1598                .unwrap();
1599        receiver.max_decompressed_size = data.len() - 1;
1600
1601        let request_data = receiver
1602            .accept(1001.0)
1603            .into_iter()
1604            .find_map(|a| match a {
1605                ResourceAction::SendRequest(d) => Some(d),
1606                _ => None,
1607            })
1608            .unwrap();
1609
1610        let send_actions = sender.handle_request(&request_data, 1002.0);
1611        receiver.req_sent = 1001.0;
1612        for action in &send_actions {
1613            if let ResourceAction::SendPart(part_data) = action {
1614                receiver.receive_part(part_data, 1003.0);
1615            }
1616        }
1617
1618        let assemble_actions = receiver.assemble(&identity_decrypt, &ExpandingCompressor);
1619        assert_eq!(receiver.status, ResourceStatus::Corrupt);
1620        assert!(assemble_actions
1621            .iter()
1622            .any(|a| matches!(a, ResourceAction::SendCancelReceiver(_))));
1623        assert!(assemble_actions
1624            .iter()
1625            .any(|a| matches!(a, ResourceAction::Failed(ResourceError::TooLarge))));
1626        assert!(assemble_actions
1627            .iter()
1628            .any(|a| matches!(a, ResourceAction::TeardownLink)));
1629    }
1630
1631    #[test]
1632    fn test_full_transfer_small_data() {
1633        // End-to-end: sender creates, receiver accepts, parts flow, assembly completes
1634        let data = b"small data";
1635        let mut rng = rns_crypto::FixedRng::new(&[0x77; 64]);
1636
1637        let mut sender = ResourceSender::new(
1638            data,
1639            None,
1640            RESOURCE_SDU,
1641            &identity_encrypt,
1642            &NoopCompressor,
1643            &mut rng,
1644            1000.0,
1645            false,
1646            false,
1647            None,
1648            1,
1649            1,
1650            None,
1651            0.5,
1652            6.0,
1653        )
1654        .unwrap();
1655
1656        let adv = sender.get_advertisement(0);
1657        let mut receiver =
1658            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
1659                .unwrap();
1660
1661        // Accept
1662        let req_actions = receiver.accept(1001.0);
1663        assert_eq!(receiver.status, ResourceStatus::Transferring);
1664
1665        // Get request data
1666        let request_data = req_actions
1667            .iter()
1668            .find_map(|a| match a {
1669                ResourceAction::SendRequest(d) => Some(d.clone()),
1670                _ => None,
1671            })
1672            .unwrap();
1673
1674        // Sender handles request
1675        let send_actions = sender.handle_request(&request_data, 1002.0);
1676
1677        // Feed all parts to receiver
1678        receiver.req_sent = 1001.0;
1679        for action in &send_actions {
1680            if let ResourceAction::SendPart(part_data) = action {
1681                receiver.receive_part(part_data, 1003.0);
1682            }
1683        }
1684
1685        assert_eq!(receiver.received_count, receiver.total_parts);
1686
1687        // Assemble
1688        let assemble_actions = receiver.assemble(&identity_decrypt, &NoopCompressor);
1689
1690        // Check for proof and data
1691        let has_proof = assemble_actions
1692            .iter()
1693            .any(|a| matches!(a, ResourceAction::SendProof(_)));
1694        let has_data = assemble_actions
1695            .iter()
1696            .any(|a| matches!(a, ResourceAction::DataReceived { .. }));
1697        let has_complete = assemble_actions
1698            .iter()
1699            .any(|a| matches!(a, ResourceAction::Completed));
1700
1701        assert!(has_proof, "Should send proof");
1702        assert!(has_data, "Should return data");
1703        assert!(has_complete, "Should be completed");
1704
1705        // Verify data matches
1706        let received_data = assemble_actions
1707            .iter()
1708            .find_map(|a| match a {
1709                ResourceAction::DataReceived { data, .. } => Some(data.clone()),
1710                _ => None,
1711            })
1712            .unwrap();
1713        assert_eq!(received_data, data);
1714
1715        // Verify proof validates on sender side
1716        let proof_data = assemble_actions
1717            .iter()
1718            .find_map(|a| match a {
1719                ResourceAction::SendProof(d) => Some(d.clone()),
1720                _ => None,
1721            })
1722            .unwrap();
1723
1724        let _proof_actions = sender.handle_proof(&proof_data, 1004.0);
1725        assert_eq!(sender.status, ResourceStatus::Complete);
1726    }
1727
1728    #[test]
1729    fn test_full_transfer_with_metadata() {
1730        let data = b"data with metadata";
1731        let metadata = b"some metadata";
1732        let mut rng = rns_crypto::FixedRng::new(&[0x88; 64]);
1733
1734        let mut sender = ResourceSender::new(
1735            data,
1736            Some(metadata),
1737            RESOURCE_SDU,
1738            &identity_encrypt,
1739            &NoopCompressor,
1740            &mut rng,
1741            1000.0,
1742            false,
1743            false,
1744            None,
1745            1,
1746            1,
1747            None,
1748            0.5,
1749            6.0,
1750        )
1751        .unwrap();
1752
1753        assert!(sender.flags.has_metadata);
1754
1755        let adv = sender.get_advertisement(0);
1756        let mut receiver =
1757            ResourceReceiver::from_advertisement(&adv, RESOURCE_SDU, 0.5, 1000.0, None, None)
1758                .unwrap();
1759
1760        assert!(receiver.has_metadata);
1761
1762        // Transfer all parts
1763        let req_actions = receiver.accept(1001.0);
1764        let request_data = req_actions
1765            .iter()
1766            .find_map(|a| match a {
1767                ResourceAction::SendRequest(d) => Some(d.clone()),
1768                _ => None,
1769            })
1770            .unwrap();
1771
1772        let send_actions = sender.handle_request(&request_data, 1002.0);
1773        receiver.req_sent = 1001.0;
1774        for action in &send_actions {
1775            if let ResourceAction::SendPart(part_data) = action {
1776                receiver.receive_part(part_data, 1003.0);
1777            }
1778        }
1779
1780        let assemble_actions = receiver.assemble(&identity_decrypt, &NoopCompressor);
1781
1782        let (recv_data, recv_meta) = assemble_actions
1783            .iter()
1784            .find_map(|a| match a {
1785                ResourceAction::DataReceived { data, metadata } => {
1786                    Some((data.clone(), metadata.clone()))
1787                }
1788                _ => None,
1789            })
1790            .unwrap();
1791
1792        assert_eq!(recv_data, data);
1793        assert_eq!(recv_meta.unwrap(), metadata);
1794    }
1795
1796    #[test]
1797    fn test_previous_window_restore() {
1798        let (_, _receiver) = make_sender_receiver();
1799        // Create with previous window
1800        let adv_data = {
1801            let mut rng = rns_crypto::FixedRng::new(&[0x42; 64]);
1802            let sender = ResourceSender::new(
1803                b"test",
1804                None,
1805                RESOURCE_SDU,
1806                &identity_encrypt,
1807                &NoopCompressor,
1808                &mut rng,
1809                1000.0,
1810                false,
1811                false,
1812                None,
1813                1,
1814                1,
1815                None,
1816                0.5,
1817                6.0,
1818            )
1819            .unwrap();
1820            sender.get_advertisement(0)
1821        };
1822
1823        let receiver = ResourceReceiver::from_advertisement(
1824            &adv_data,
1825            RESOURCE_SDU,
1826            0.5,
1827            1000.0,
1828            Some(8),
1829            Some(50000.0),
1830        )
1831        .unwrap();
1832        assert_eq!(receiver.window.window, 8);
1833    }
1834
1835    #[test]
1836    fn test_tick_timeout_retry() {
1837        let (_, mut receiver) = make_sender_receiver();
1838        receiver.accept(1000.0);
1839        receiver.rtt = Some(0.1);
1840
1841        // Way past timeout
1842        let actions = receiver.tick(9999.0, &identity_decrypt, &NoopCompressor);
1843        // Should have retried (window decreased, request_next called)
1844        assert!(!actions.is_empty() || receiver.retries_left < RESOURCE_MAX_RETRIES);
1845    }
1846
1847    #[test]
1848    fn test_tick_waiting_for_hmu_gets_extra_timeout() {
1849        let (_, mut receiver) = make_sender_receiver();
1850        receiver.accept(1000.0);
1851        receiver.waiting_for_hmu = true;
1852        receiver.outstanding_parts = 0;
1853        let eifr = 10_000.0;
1854        receiver.previous_eifr = Some(eifr);
1855        receiver.last_activity = 1000.0;
1856
1857        let old_timeout = base_timeout(&receiver, eifr);
1858        let now = old_timeout + 0.01;
1859
1860        let actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
1861
1862        assert!(actions.is_empty(), "receiver should keep waiting for HMU");
1863        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES);
1864        assert_eq!(receiver.status, ResourceStatus::Transferring);
1865    }
1866
1867    #[test]
1868    fn test_tick_zero_outstanding_parts_gets_extra_timeout_without_hmu_flag() {
1869        let (_, mut receiver) = make_sender_receiver();
1870        receiver.accept(1000.0);
1871        receiver.waiting_for_hmu = false;
1872        receiver.outstanding_parts = 0;
1873        let eifr = 10_000.0;
1874        receiver.previous_eifr = Some(eifr);
1875        receiver.last_activity = 1000.0;
1876
1877        let old_timeout = base_timeout(&receiver, eifr);
1878        let now = old_timeout + 0.01;
1879
1880        let actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
1881
1882        assert!(
1883            actions.is_empty(),
1884            "receiver should keep waiting for follow-up hashmap data"
1885        );
1886        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES);
1887        assert_eq!(receiver.status, ResourceStatus::Transferring);
1888    }
1889
1890    #[test]
1891    fn test_tick_waiting_for_hmu_retries_after_extended_timeout() {
1892        let (_, mut receiver) = make_sender_receiver();
1893        receiver.accept(1000.0);
1894        receiver.waiting_for_hmu = true;
1895        receiver.outstanding_parts = 0;
1896        let eifr = 10_000.0;
1897        receiver.previous_eifr = Some(eifr);
1898        receiver.last_activity = 1000.0;
1899
1900        let now = hmu_timeout(&receiver, eifr) + 0.01;
1901        let _actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
1902
1903        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES - 1);
1904        assert!(!receiver.waiting_for_hmu);
1905    }
1906
1907    #[test]
1908    fn test_tick_inflight_parts_do_not_get_hmu_timeout_extension() {
1909        let (_, mut receiver) = make_sender_receiver();
1910        receiver.accept(1000.0);
1911        receiver.waiting_for_hmu = false;
1912        receiver.outstanding_parts = 2;
1913        let eifr = 10_000.0;
1914        receiver.previous_eifr = Some(eifr);
1915        receiver.last_activity = 1000.0;
1916
1917        let now = base_timeout(&receiver, eifr) + 0.01;
1918        let _actions = receiver.tick(now, &identity_decrypt, &NoopCompressor);
1919
1920        assert_eq!(receiver.retries_left, RESOURCE_MAX_RETRIES - 1);
1921    }
1922
1923    #[test]
1924    fn test_tick_max_retries_exceeded() {
1925        let (_, mut receiver) = make_sender_receiver();
1926        receiver.accept(1000.0);
1927        receiver.retries_left = 0;
1928        receiver.rtt = Some(0.001);
1929        receiver.eifr = Some(100000.0);
1930
1931        let _actions = receiver.tick(9999.0, &identity_decrypt, &NoopCompressor);
1932        assert_eq!(receiver.status, ResourceStatus::Failed);
1933    }
1934}