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