Skip to main content

ipfrs_network/
block_transfer.rs

1//! Streaming block transfer management with chunked, resumable transfers.
2//!
3//! Provides [`StreamingBlockTransfer`] for managing chunked, resumable block
4//! transfers between peers with progress tracking and checksum verification.
5
6use std::collections::HashMap;
7
8/// Direction of a block transfer.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum TransferDirection {
11    /// Uploading data to a remote peer.
12    Upload,
13    /// Downloading data from a remote peer.
14    Download,
15}
16
17/// A single chunk of data in a block transfer.
18#[derive(Clone, Debug)]
19pub struct TransferChunk {
20    /// Zero-based index of this chunk in the overall transfer.
21    pub chunk_index: u32,
22    /// Raw bytes of this chunk.
23    pub data: Vec<u8>,
24    /// Simple checksum: XOR of all bytes cast to u32.
25    pub checksum: u32,
26}
27
28impl TransferChunk {
29    /// Compute the expected checksum for the contained data.
30    pub fn compute_checksum(data: &[u8]) -> u32 {
31        data.iter().fold(0u32, |acc, &b| acc ^ u32::from(b))
32    }
33
34    /// Return `true` if the stored checksum matches the data.
35    pub fn is_valid(&self) -> bool {
36        self.checksum == Self::compute_checksum(&self.data)
37    }
38}
39
40/// Lifecycle state of a [`BlockTransfer`].
41#[derive(Clone, Debug, PartialEq)]
42pub enum TransferState {
43    /// Transfer has been created but not yet started.
44    Pending,
45    /// Transfer is actively exchanging chunks.
46    InProgress {
47        /// Number of chunks successfully processed so far.
48        chunks_done: u32,
49        /// Total number of chunks in the transfer.
50        total_chunks: u32,
51    },
52    /// Transfer has been paused mid-way.
53    Paused {
54        /// Number of chunks processed before pausing.
55        chunks_done: u32,
56    },
57    /// All chunks have been received/sent successfully.
58    Completed,
59    /// Transfer encountered an unrecoverable error.
60    Failed {
61        /// Human-readable description of the failure.
62        reason: String,
63    },
64}
65
66/// A single block transfer between the local node and a remote peer.
67#[derive(Clone, Debug)]
68pub struct BlockTransfer {
69    /// Unique identifier for this transfer.
70    pub transfer_id: u64,
71    /// Content identifier of the block being transferred.
72    pub cid: String,
73    /// Identifier of the remote peer.
74    pub peer_id: String,
75    /// Whether this node is uploading or downloading.
76    pub direction: TransferDirection,
77    /// Total size of the block in bytes.
78    pub total_bytes: u64,
79    /// Maximum bytes per chunk (default 65536).
80    pub chunk_size: u64,
81    /// Current lifecycle state of the transfer.
82    pub state: TransferState,
83    /// Indices of chunks that have been received or sent.
84    pub received_chunks: Vec<u32>,
85    /// Tick at which the transfer was started.
86    pub started_at_tick: u64,
87}
88
89impl BlockTransfer {
90    /// Return the total number of chunks required for this transfer.
91    ///
92    /// Uses ceiling division so that a partial last chunk is counted.
93    pub fn total_chunks(&self) -> u32 {
94        if self.chunk_size == 0 {
95            return 0;
96        }
97        self.total_bytes.div_ceil(self.chunk_size) as u32
98    }
99
100    /// Return transfer progress as a fraction in `[0.0, 1.0]`.
101    ///
102    /// Returns `0.0` when the transfer is [`TransferState::Pending`].
103    pub fn progress(&self) -> f64 {
104        let total = self.total_chunks();
105        if total == 0 {
106            return 1.0;
107        }
108        let done = match &self.state {
109            TransferState::Pending => 0,
110            TransferState::InProgress { chunks_done, .. } => *chunks_done,
111            TransferState::Paused { chunks_done } => *chunks_done,
112            TransferState::Completed => total,
113            TransferState::Failed { .. } => self.received_chunks.len() as u32,
114        };
115        f64::from(done) / f64::from(total)
116    }
117
118    /// Return `true` if the transfer has reached [`TransferState::Completed`].
119    pub fn is_complete(&self) -> bool {
120        self.state == TransferState::Completed
121    }
122}
123
124/// Aggregated statistics for all transfers managed by a [`StreamingBlockTransfer`].
125#[derive(Clone, Debug, Default)]
126pub struct TransferManagerStats {
127    /// Number of transfers that are currently active (Pending, InProgress, or Paused).
128    pub active_transfers: usize,
129    /// Total number of transfers that reached [`TransferState::Completed`].
130    pub completed_transfers: u64,
131    /// Total number of transfers that reached [`TransferState::Failed`].
132    pub failed_transfers: u64,
133    /// Cumulative bytes transferred across all completed transfers.
134    pub total_bytes_transferred: u64,
135}
136
137/// Manager for chunked, resumable block transfers between peers.
138///
139/// Each transfer is identified by a unique `u64` transfer ID returned by
140/// [`StreamingBlockTransfer::start_transfer`].
141pub struct StreamingBlockTransfer {
142    /// All tracked transfers indexed by their transfer ID.
143    pub transfers: HashMap<u64, BlockTransfer>,
144    /// Counter used to assign the next unique transfer ID.
145    pub next_id: u64,
146    /// Monotonically increasing logical clock.
147    pub current_tick: u64,
148}
149
150impl StreamingBlockTransfer {
151    /// Create a new, empty transfer manager.
152    pub fn new() -> Self {
153        Self {
154            transfers: HashMap::new(),
155            next_id: 1,
156            current_tick: 0,
157        }
158    }
159
160    /// Register a new transfer and return its assigned transfer ID.
161    ///
162    /// The transfer begins in the [`TransferState::Pending`] state.
163    /// If `chunk_size` is `0`, it defaults to `65536`.
164    pub fn start_transfer(
165        &mut self,
166        cid: String,
167        peer_id: String,
168        direction: TransferDirection,
169        total_bytes: u64,
170        chunk_size: u64,
171    ) -> u64 {
172        let id = self.next_id;
173        self.next_id += 1;
174
175        let effective_chunk_size = if chunk_size == 0 { 65536 } else { chunk_size };
176
177        let transfer = BlockTransfer {
178            transfer_id: id,
179            cid,
180            peer_id,
181            direction,
182            total_bytes,
183            chunk_size: effective_chunk_size,
184            state: TransferState::Pending,
185            received_chunks: Vec::new(),
186            started_at_tick: self.current_tick,
187        };
188
189        self.transfers.insert(id, transfer);
190        id
191    }
192
193    /// Process an incoming chunk for the given transfer.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error string when:
198    /// - The transfer ID is unknown.
199    /// - The transfer is not in an `InProgress` or `Pending` state.
200    /// - The chunk checksum does not match its data.
201    /// - The chunk index has already been received.
202    pub fn receive_chunk(&mut self, transfer_id: u64, chunk: TransferChunk) -> Result<(), String> {
203        let transfer = self
204            .transfers
205            .get_mut(&transfer_id)
206            .ok_or_else(|| format!("unknown transfer id: {transfer_id}"))?;
207
208        // Validate state: only Pending and InProgress accept new chunks.
209        match &transfer.state {
210            TransferState::Paused { .. } => {
211                return Err(format!("transfer {transfer_id} is paused"));
212            }
213            TransferState::Completed => {
214                return Err(format!("transfer {transfer_id} is already completed"));
215            }
216            TransferState::Failed { reason } => {
217                return Err(format!("transfer {transfer_id} has failed: {reason}"));
218            }
219            TransferState::Pending | TransferState::InProgress { .. } => {}
220        }
221
222        // Validate checksum.
223        if !chunk.is_valid() {
224            return Err(format!(
225                "checksum mismatch for chunk {} of transfer {transfer_id}: expected {}, got {}",
226                chunk.chunk_index,
227                TransferChunk::compute_checksum(&chunk.data),
228                chunk.checksum,
229            ));
230        }
231
232        // Reject duplicates.
233        if transfer.received_chunks.contains(&chunk.chunk_index) {
234            return Err(format!(
235                "chunk {} already received for transfer {transfer_id}",
236                chunk.chunk_index
237            ));
238        }
239
240        transfer.received_chunks.push(chunk.chunk_index);
241        let chunks_done = transfer.received_chunks.len() as u32;
242        let total = transfer.total_chunks();
243
244        if chunks_done >= total {
245            transfer.state = TransferState::Completed;
246        } else {
247            transfer.state = TransferState::InProgress {
248                chunks_done,
249                total_chunks: total,
250            };
251        }
252
253        Ok(())
254    }
255
256    /// Pause an active or pending transfer.
257    ///
258    /// Returns `true` if the transfer was successfully paused, `false` otherwise
259    /// (e.g., the transfer ID is unknown, or the transfer is already paused/done).
260    pub fn pause(&mut self, transfer_id: u64) -> bool {
261        let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
262            return false;
263        };
264
265        let chunks_done = match &transfer.state {
266            TransferState::Pending => 0,
267            TransferState::InProgress { chunks_done, .. } => *chunks_done,
268            _ => return false,
269        };
270
271        transfer.state = TransferState::Paused { chunks_done };
272        true
273    }
274
275    /// Resume a paused transfer.
276    ///
277    /// Returns `true` if the transfer was successfully resumed, `false` otherwise.
278    pub fn resume(&mut self, transfer_id: u64) -> bool {
279        let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
280            return false;
281        };
282
283        let chunks_done = match &transfer.state {
284            TransferState::Paused { chunks_done } => *chunks_done,
285            _ => return false,
286        };
287
288        let total_chunks = transfer.total_chunks();
289        transfer.state = TransferState::InProgress {
290            chunks_done,
291            total_chunks,
292        };
293        true
294    }
295
296    /// Mark a transfer as failed with a descriptive reason.
297    ///
298    /// Returns `true` if the transfer was found and transitioned to
299    /// [`TransferState::Failed`], `false` if the ID is unknown or the
300    /// transfer has already completed/failed.
301    pub fn fail(&mut self, transfer_id: u64, reason: String) -> bool {
302        let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
303            return false;
304        };
305
306        match transfer.state {
307            TransferState::Completed | TransferState::Failed { .. } => return false,
308            _ => {}
309        }
310
311        transfer.state = TransferState::Failed { reason };
312        true
313    }
314
315    /// Compute aggregated statistics across all managed transfers.
316    pub fn stats(&self) -> TransferManagerStats {
317        let mut stats = TransferManagerStats::default();
318
319        for transfer in self.transfers.values() {
320            match &transfer.state {
321                TransferState::Pending
322                | TransferState::InProgress { .. }
323                | TransferState::Paused { .. } => {
324                    stats.active_transfers += 1;
325                }
326                TransferState::Completed => {
327                    stats.completed_transfers += 1;
328                    stats.total_bytes_transferred += transfer.total_bytes;
329                }
330                TransferState::Failed { .. } => {
331                    stats.failed_transfers += 1;
332                }
333            }
334        }
335
336        stats
337    }
338
339    /// Advance the logical clock by one tick.
340    pub fn advance_tick(&mut self) {
341        self.current_tick += 1;
342    }
343}
344
345impl Default for StreamingBlockTransfer {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351// ---------------------------------------------------------------------------
352// Tests
353// ---------------------------------------------------------------------------
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    fn make_chunk(index: u32, data: Vec<u8>) -> TransferChunk {
360        let checksum = TransferChunk::compute_checksum(&data);
361        TransferChunk {
362            chunk_index: index,
363            data,
364            checksum,
365        }
366    }
367
368    // 1. start_transfer returns unique, incrementing IDs.
369    #[test]
370    fn test_start_transfer_unique_ids() {
371        let mut mgr = StreamingBlockTransfer::new();
372        let id1 = mgr.start_transfer(
373            "cid1".into(),
374            "peer1".into(),
375            TransferDirection::Download,
376            1024,
377            512,
378        );
379        let id2 = mgr.start_transfer(
380            "cid2".into(),
381            "peer2".into(),
382            TransferDirection::Upload,
383            2048,
384            512,
385        );
386        assert_ne!(id1, id2);
387        assert!(id2 > id1);
388    }
389
390    // 2. New transfer starts in Pending state.
391    #[test]
392    fn test_start_transfer_pending_state() {
393        let mut mgr = StreamingBlockTransfer::new();
394        let id = mgr.start_transfer(
395            "cid".into(),
396            "peer".into(),
397            TransferDirection::Download,
398            100,
399            64,
400        );
401        let t = &mgr.transfers[&id];
402        assert_eq!(t.state, TransferState::Pending);
403    }
404
405    // 3. total_chunks calculation — exact multiple.
406    #[test]
407    fn test_total_chunks_exact_multiple() {
408        let mut mgr = StreamingBlockTransfer::new();
409        let id = mgr.start_transfer(
410            "c".into(),
411            "p".into(),
412            TransferDirection::Download,
413            65536,
414            65536,
415        );
416        assert_eq!(mgr.transfers[&id].total_chunks(), 1);
417    }
418
419    // 4. total_chunks calculation — partial last chunk.
420    #[test]
421    fn test_total_chunks_partial() {
422        let mut mgr = StreamingBlockTransfer::new();
423        let id = mgr.start_transfer(
424            "c".into(),
425            "p".into(),
426            TransferDirection::Download,
427            65537,
428            65536,
429        );
430        assert_eq!(mgr.transfers[&id].total_chunks(), 2);
431    }
432
433    // 5. total_chunks for zero bytes.
434    #[test]
435    fn test_total_chunks_zero_bytes() {
436        let mut mgr = StreamingBlockTransfer::new();
437        let id = mgr.start_transfer(
438            "c".into(),
439            "p".into(),
440            TransferDirection::Download,
441            0,
442            65536,
443        );
444        assert_eq!(mgr.transfers[&id].total_chunks(), 0);
445    }
446
447    // 6. receive_chunk updates state to InProgress.
448    #[test]
449    fn test_receive_chunk_updates_state() {
450        let mut mgr = StreamingBlockTransfer::new();
451        // 2 chunks of 10 bytes each = 20 bytes total.
452        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
453        let chunk = make_chunk(0, vec![1u8; 10]);
454        mgr.receive_chunk(id, chunk)
455            .expect("receive_chunk should succeed");
456        match &mgr.transfers[&id].state {
457            TransferState::InProgress {
458                chunks_done,
459                total_chunks,
460            } => {
461                assert_eq!(*chunks_done, 1);
462                assert_eq!(*total_chunks, 2);
463            }
464            other => panic!("expected InProgress, got {other:?}"),
465        }
466    }
467
468    // 7. receive_chunk with bad checksum returns Err.
469    #[test]
470    fn test_receive_chunk_checksum_mismatch() {
471        let mut mgr = StreamingBlockTransfer::new();
472        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
473        let chunk = TransferChunk {
474            chunk_index: 0,
475            data: vec![42u8; 10],
476            checksum: 0xDEAD_BEEF, // deliberately wrong
477        };
478        let result = mgr.receive_chunk(id, chunk);
479        assert!(result.is_err(), "expected error for checksum mismatch");
480        assert!(result.unwrap_err().contains("checksum mismatch"));
481    }
482
483    // 8. All chunks received → Completed.
484    #[test]
485    fn test_all_chunks_received_completes_transfer() {
486        let mut mgr = StreamingBlockTransfer::new();
487        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
488        for i in 0..3u32 {
489            let chunk = make_chunk(i, vec![i as u8; 10]);
490            mgr.receive_chunk(id, chunk)
491                .expect("chunk should be accepted");
492        }
493        assert_eq!(mgr.transfers[&id].state, TransferState::Completed);
494        assert!(mgr.transfers[&id].is_complete());
495    }
496
497    // 9. Duplicate chunk rejected.
498    #[test]
499    fn test_duplicate_chunk_rejected() {
500        let mut mgr = StreamingBlockTransfer::new();
501        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
502        let chunk0 = make_chunk(0, vec![1u8; 10]);
503        mgr.receive_chunk(id, chunk0.clone())
504            .expect("first receive should succeed");
505        let dup = make_chunk(0, vec![1u8; 10]);
506        let result = mgr.receive_chunk(id, dup);
507        assert!(result.is_err());
508        assert!(result.unwrap_err().contains("already received"));
509    }
510
511    // 10. pause transitions InProgress → Paused.
512    #[test]
513    fn test_pause_in_progress() {
514        let mut mgr = StreamingBlockTransfer::new();
515        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
516        let chunk = make_chunk(0, vec![0u8; 10]);
517        mgr.receive_chunk(id, chunk)
518            .expect("test: receive_chunk should accept valid chunk");
519        assert!(mgr.pause(id));
520        match &mgr.transfers[&id].state {
521            TransferState::Paused { chunks_done } => assert_eq!(*chunks_done, 1),
522            other => panic!("expected Paused, got {other:?}"),
523        }
524    }
525
526    // 11. pause on Pending → Paused(0).
527    #[test]
528    fn test_pause_pending() {
529        let mut mgr = StreamingBlockTransfer::new();
530        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 100, 10);
531        assert!(mgr.pause(id));
532        assert_eq!(
533            mgr.transfers[&id].state,
534            TransferState::Paused { chunks_done: 0 }
535        );
536    }
537
538    // 12. resume transitions Paused → InProgress.
539    #[test]
540    fn test_resume_paused() {
541        let mut mgr = StreamingBlockTransfer::new();
542        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
543        let chunk = make_chunk(0, vec![7u8; 10]);
544        mgr.receive_chunk(id, chunk)
545            .expect("test: receive_chunk should accept valid chunk");
546        mgr.pause(id);
547        assert!(mgr.resume(id));
548        match &mgr.transfers[&id].state {
549            TransferState::InProgress {
550                chunks_done,
551                total_chunks,
552            } => {
553                assert_eq!(*chunks_done, 1);
554                assert_eq!(*total_chunks, 3);
555            }
556            other => panic!("expected InProgress, got {other:?}"),
557        }
558    }
559
560    // 13. resume returns false for non-paused transfer.
561    #[test]
562    fn test_resume_non_paused_returns_false() {
563        let mut mgr = StreamingBlockTransfer::new();
564        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
565        assert!(!mgr.resume(id)); // still Pending
566    }
567
568    // 14. fail sets Failed state.
569    #[test]
570    fn test_fail_sets_failed_state() {
571        let mut mgr = StreamingBlockTransfer::new();
572        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 100, 10);
573        assert!(mgr.fail(id, "network timeout".into()));
574        match &mgr.transfers[&id].state {
575            TransferState::Failed { reason } => assert_eq!(reason, "network timeout"),
576            other => panic!("expected Failed, got {other:?}"),
577        }
578    }
579
580    // 15. fail on completed transfer returns false.
581    #[test]
582    fn test_fail_on_completed_returns_false() {
583        let mut mgr = StreamingBlockTransfer::new();
584        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
585        let chunk = make_chunk(0, vec![0u8; 10]);
586        mgr.receive_chunk(id, chunk)
587            .expect("test: receive_chunk should accept valid chunk");
588        assert!(mgr.transfers[&id].is_complete());
589        assert!(!mgr.fail(id, "too late".into()));
590    }
591
592    // 16. progress before and after receiving chunks.
593    #[test]
594    fn test_progress_values() {
595        let mut mgr = StreamingBlockTransfer::new();
596        // 4 chunks of 10 bytes.
597        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 40, 10);
598        assert_eq!(mgr.transfers[&id].progress(), 0.0);
599
600        let chunk0 = make_chunk(0, vec![1u8; 10]);
601        mgr.receive_chunk(id, chunk0)
602            .expect("test: receive_chunk should accept chunk 0");
603        let p = mgr.transfers[&id].progress();
604        assert!((p - 0.25).abs() < f64::EPSILON, "expected 0.25 got {p}");
605
606        let chunk1 = make_chunk(1, vec![2u8; 10]);
607        mgr.receive_chunk(id, chunk1)
608            .expect("test: receive_chunk should accept chunk 1");
609        let p = mgr.transfers[&id].progress();
610        assert!((p - 0.5).abs() < f64::EPSILON, "expected 0.5 got {p}");
611    }
612
613    // 17. stats counts active, completed, and failed correctly.
614    #[test]
615    fn test_stats_counts() {
616        let mut mgr = StreamingBlockTransfer::new();
617
618        // Two active (Pending).
619        mgr.start_transfer(
620            "c1".into(),
621            "p1".into(),
622            TransferDirection::Download,
623            10,
624            10,
625        );
626        mgr.start_transfer("c2".into(), "p2".into(), TransferDirection::Upload, 10, 10);
627
628        // One completed.
629        let id_done = mgr.start_transfer(
630            "c3".into(),
631            "p3".into(),
632            TransferDirection::Download,
633            10,
634            10,
635        );
636        let chunk = make_chunk(0, vec![5u8; 10]);
637        mgr.receive_chunk(id_done, chunk)
638            .expect("test: receive_chunk should accept valid chunk for completed transfer");
639
640        // One failed.
641        let id_fail = mgr.start_transfer(
642            "c4".into(),
643            "p4".into(),
644            TransferDirection::Download,
645            10,
646            10,
647        );
648        mgr.fail(id_fail, "oops".into());
649
650        let stats = mgr.stats();
651        assert_eq!(stats.active_transfers, 2);
652        assert_eq!(stats.completed_transfers, 1);
653        assert_eq!(stats.failed_transfers, 1);
654        assert_eq!(stats.total_bytes_transferred, 10);
655    }
656
657    // 18. advance_tick increments current_tick.
658    #[test]
659    fn test_advance_tick() {
660        let mut mgr = StreamingBlockTransfer::new();
661        assert_eq!(mgr.current_tick, 0);
662        mgr.advance_tick();
663        mgr.advance_tick();
664        assert_eq!(mgr.current_tick, 2);
665    }
666
667    // 19. started_at_tick is recorded from current_tick.
668    #[test]
669    fn test_started_at_tick_recorded() {
670        let mut mgr = StreamingBlockTransfer::new();
671        mgr.advance_tick();
672        mgr.advance_tick();
673        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Upload, 10, 10);
674        assert_eq!(mgr.transfers[&id].started_at_tick, 2);
675    }
676
677    // 20. receive_chunk rejected for paused transfer.
678    #[test]
679    fn test_receive_chunk_rejected_when_paused() {
680        let mut mgr = StreamingBlockTransfer::new();
681        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
682        mgr.pause(id);
683        let chunk = make_chunk(0, vec![3u8; 10]);
684        let result = mgr.receive_chunk(id, chunk);
685        assert!(result.is_err());
686        assert!(result.unwrap_err().contains("paused"));
687    }
688
689    // 21. is_complete returns false for InProgress.
690    #[test]
691    fn test_is_complete_false_for_in_progress() {
692        let mut mgr = StreamingBlockTransfer::new();
693        let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
694        let chunk = make_chunk(0, vec![9u8; 10]);
695        mgr.receive_chunk(id, chunk)
696            .expect("test: receive_chunk should accept valid chunk");
697        assert!(!mgr.transfers[&id].is_complete());
698    }
699
700    // 22. Unknown transfer ID returns error from receive_chunk.
701    #[test]
702    fn test_receive_chunk_unknown_id() {
703        let mut mgr = StreamingBlockTransfer::new();
704        let chunk = make_chunk(0, vec![1u8]);
705        let result = mgr.receive_chunk(9999, chunk);
706        assert!(result.is_err());
707        assert!(result.unwrap_err().contains("unknown transfer id"));
708    }
709}