Skip to main content

scp2p_core/
transfer.rs

1// Copyright (c) 2024-2026 Vanyo Vanev / Tech Art Ltd
2// SPDX-License-Identifier: MPL-2.0
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7use crate::{
8    content::{CHUNK_SIZE, verify_chunk, verify_content},
9    ids::ContentId,
10    peer::PeerAddr,
11};
12
13#[derive(Debug, Clone)]
14pub struct ChunkProvider {
15    pub peer: PeerAddr,
16    pub content_bytes: Vec<u8>,
17}
18
19pub fn download_swarm(
20    content_id: [u8; 32],
21    chunk_hashes: &[[u8; 32]],
22    providers: &[ChunkProvider],
23) -> anyhow::Result<Vec<u8>> {
24    if providers.is_empty() {
25        anyhow::bail!("no providers available");
26    }
27
28    let mut output = Vec::new();
29    for (idx, expected_hash) in chunk_hashes.iter().enumerate() {
30        let mut chunk = None;
31
32        for offset in 0..providers.len() {
33            let provider_idx = (idx + offset) % providers.len();
34            if let Some(candidate) = chunk_from_provider(&providers[provider_idx], idx)
35                && verify_chunk(expected_hash, candidate).is_ok()
36            {
37                chunk = Some(candidate.to_vec());
38                break;
39            }
40        }
41
42        let Some(bytes) = chunk else {
43            anyhow::bail!("unable to retrieve verified chunk {idx}");
44        };
45        output.extend_from_slice(&bytes);
46    }
47
48    verify_content(&ContentId(content_id), &output)?;
49    Ok(output)
50}
51
52fn chunk_from_provider(provider: &ChunkProvider, idx: usize) -> Option<&[u8]> {
53    let start = idx * CHUNK_SIZE;
54    if start >= provider.content_bytes.len() {
55        return None;
56    }
57    let end = ((idx + 1) * CHUNK_SIZE).min(provider.content_bytes.len());
58    Some(&provider.content_bytes[start..end])
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::content::describe_content;
65    use crate::peer::TransportProtocol;
66
67    fn provider(ip: &str, bytes: Vec<u8>) -> ChunkProvider {
68        ChunkProvider {
69            peer: PeerAddr {
70                ip: ip.parse().expect("valid ip"),
71                port: 7000,
72                transport: TransportProtocol::Quic,
73                pubkey_hint: None,
74                relay_via: None,
75            },
76            content_bytes: bytes,
77        }
78    }
79
80    #[test]
81    fn swarm_download_verifies_and_recovers() {
82        let data = vec![1u8; CHUNK_SIZE + 17];
83        let desc = describe_content(&data);
84        let providers = vec![
85            provider("10.0.0.1", data.clone()),
86            provider("10.0.0.2", data.clone()),
87        ];
88
89        let out = download_swarm(desc.content_id.0, &desc.chunks, &providers).expect("download");
90        assert_eq!(out, data);
91    }
92
93    #[test]
94    fn swarm_download_rejects_corrupted_only_sources() {
95        let data = vec![2u8; CHUNK_SIZE + 3];
96        let desc = describe_content(&data);
97        let mut bad = data.clone();
98        bad[0] ^= 1;
99
100        let err = download_swarm(
101            desc.content_id.0,
102            &desc.chunks,
103            &[provider("10.0.0.3", bad)],
104        )
105        .expect_err("must fail");
106        assert!(err.to_string().contains("chunk 0"));
107    }
108}