Skip to main content

clawft_kernel/
mesh_artifact_transfer.rs

1//! Serve and fetch for the artifact piece protocol (mesh-placement-11).
2//!
3//! One [`MeshStream`] carries a client-driven exchange:
4//!
5//! ```text
6//! fetcher                              holder
7//!   meta_request(key)        0x0B  ->
8//!                            <-  0x0C  meta(descriptor) | reject
9//!                            <-  0x0B  announce(have)
10//!   announce(have)           0x0B  ->
11//!   request(id, [i..])       0x0B  ->
12//!                            <-  0x0C  piece(i, offset, block)... | no_piece(i)
13//! ```
14//!
15//! Every piece is hash-checked before it reaches `ArtifactStore`; a bad
16//! piece is chained (`artifact.piece_rejected`) and requested again. When
17//! all pieces are held the whole-content hash is checked. A peer whose
18//! descriptor fails that check lied about the content: its descriptor and
19//! unshared pieces are discarded, the peer is dropped and the next peer is
20//! tried. The fetch outcome is chained once (`artifact.fetch`), listing any
21//! rejected descriptors.
22//!
23//! v1 fetches from one peer at a time, walking a [`PeerSet`] in order.
24//! Card 25 plugs a multi-source [`PieceScheduler`] and parallel peers into
25//! the same types.
26
27use std::collections::{BTreeMap, HashSet};
28
29use crate::mesh::{MeshError, MeshStream};
30use crate::mesh_artifact::{ArtifactExchange, ExchangeError};
31pub use crate::mesh_artifact_peers::{
32    FetchError, FetchOutcome, PeerLink, PeerSet, PieceScheduler, SequentialScheduler, ServeStats,
33};
34use crate::mesh_artifact_wire::{ArtifactDescriptor, ArtifactKey, ArtifactMsg, Bitfield};
35use crate::mesh_swarm_state::{Audience, ServePeer};
36
37#[derive(Debug, thiserror::Error)]
38pub(crate) enum PeerError {
39    #[error("{0}")]
40    Mesh(#[from] MeshError),
41    #[error("{0}")]
42    Wire(#[from] crate::mesh_artifact_wire::WireError),
43    #[error("{0}")]
44    Local(#[from] ExchangeError),
45    #[error("protocol: {0}")]
46    Protocol(String),
47    #[error("peer refused: {0}")]
48    Refused(String),
49    #[error("timed out waiting for peer")]
50    Timeout,
51}
52
53#[derive(Default)]
54struct Progress {
55    pieces: u32,
56    bytes: u64,
57    rejected: u32,
58    sources: Vec<String>,
59    /// Descriptor of the attempt in flight (pending until verified).
60    current: Option<ArtifactDescriptor>,
61    /// `(peer, artifact id, reason)` for descriptors that failed the
62    /// whole-content check.
63    bad_descriptors: Vec<(String, String, String)>,
64    /// Piece blobs this fetch created in the store: the only blobs a
65    /// failed promotion may roll back.
66    written: HashSet<[u8; 32]>,
67}
68
69pub(crate) async fn send(
70    stream: &mut dyn MeshStream,
71    msg: &ArtifactMsg,
72) -> Result<(), PeerError> {
73    stream.send(&msg.to_wire()?).await?;
74    Ok(())
75}
76
77impl ArtifactExchange {
78    pub(crate) async fn recv(
79        &self,
80        stream: &mut dyn MeshStream,
81    ) -> Result<ArtifactMsg, PeerError> {
82        let raw = tokio::time::timeout(self.config().recv_timeout, stream.recv())
83            .await
84            .map_err(|_| PeerError::Timeout)??;
85        Ok(ArtifactMsg::from_wire(&raw)?)
86    }
87
88    /// Serve one peer until it closes the stream. Only pieces of servable
89    /// artifacts ([`Self::is_servable`]) are sent. An oversize or
90    /// malformed frame ends the session with an error, and so does a peer
91    /// that sends nothing for `serve_idle_timeout`.
92    ///
93    /// `peer` is only a claimed id (the peer is [`ServePeer::unverified`]):
94    /// with a policy that serves verified peers only, nothing is served. Use
95    /// [`Self::serve_as`] when admission authenticated the peer.
96    pub async fn serve(
97        &self,
98        stream: &mut dyn MeshStream,
99        peer: &str,
100    ) -> Result<ServeStats, ExchangeError> {
101        self.serve_as(stream, &ServePeer::unverified(peer)).await
102    }
103
104    /// [`Self::serve`] for a peer whose identity the caller has (or has not)
105    /// verified: the redistribution policy sees `peer` for every request.
106    pub async fn serve_as(
107        &self,
108        stream: &mut dyn MeshStream,
109        who: &ServePeer,
110    ) -> Result<ServeStats, ExchangeError> {
111        let peer = who.node_id.as_str();
112        let mut stats = ServeStats::default();
113        if self.is_banned(peer) {
114            let _ = stream.close().await;
115            return Err(ExchangeError::Banned(peer.to_string()));
116        }
117        loop {
118            let idle = self.config().serve_idle_timeout;
119            let raw = match tokio::time::timeout(idle, stream.recv()).await {
120                Ok(Ok(raw)) => raw,
121                Ok(Err(MeshError::ConnectionClosed)) => return Ok(stats),
122                Ok(Err(e)) => return Err(ExchangeError::Io(e.to_string())),
123                Err(_) => {
124                    let _ = stream.close().await;
125                    return Err(ExchangeError::Io(format!(
126                        "peer {peer} idle for {idle:?}; serve session closed"
127                    )));
128                }
129            };
130            let msg = match ArtifactMsg::from_wire(&raw) {
131                Ok(m) => m,
132                Err(e) => {
133                    let _ = stream.close().await;
134                    return Err(e.into());
135                }
136            };
137            if let Err(e) = self.serve_one(stream, who, msg, &mut stats).await {
138                let _ = stream.close().await;
139                return Err(match e {
140                    PeerError::Local(l) => l,
141                    other => ExchangeError::Io(other.to_string()),
142                });
143            }
144        }
145    }
146
147    /// Serve one frame already read from `stream`, for sessions that carry
148    /// the piece protocol beside other traffic on one stream (the
149    /// `workload.ctl` fetch-before-load, mesh-placement-12). Same rules as
150    /// [`Self::serve`]: only servable artifacts are sent.
151    pub async fn serve_frame(
152        &self,
153        stream: &mut dyn MeshStream,
154        peer: &str,
155        raw: &[u8],
156        stats: &mut ServeStats,
157    ) -> Result<(), ExchangeError> {
158        self.serve_frame_as(stream, &ServePeer::unverified(peer), raw, stats)
159            .await
160    }
161
162    /// [`Self::serve_frame`] with the peer's verified-ness stated.
163    pub async fn serve_frame_as(
164        &self,
165        stream: &mut dyn MeshStream,
166        who: &ServePeer,
167        raw: &[u8],
168        stats: &mut ServeStats,
169    ) -> Result<(), ExchangeError> {
170        let peer = who.node_id.as_str();
171        if self.is_banned(peer) {
172            let _ = stream.close().await;
173            return Err(ExchangeError::Banned(peer.to_string()));
174        }
175        let msg = ArtifactMsg::from_wire(raw)?;
176        self.serve_one(stream, who, msg, stats)
177            .await
178            .map_err(|e| match e {
179                PeerError::Local(l) => l,
180                other => ExchangeError::Io(other.to_string()),
181            })
182    }
183
184    async fn serve_one(
185        &self,
186        stream: &mut dyn MeshStream,
187        who: &ServePeer,
188        msg: ArtifactMsg,
189        stats: &mut ServeStats,
190    ) -> Result<(), PeerError> {
191        let peer = who.node_id.as_str();
192        match msg {
193            ArtifactMsg::MetaRequest { key } => match self.servable(&key, who) {
194                Some(d) => {
195                    let id = d.id();
196                    let have = self.have(&id).unwrap_or_else(|| Bitfield::new(0));
197                    send(stream, &ArtifactMsg::Meta { descriptor: d }).await?;
198                    send(stream, &ArtifactMsg::Announce { id, have }).await
199                }
200                None => {
201                    let reason = "not available or not servable".to_string();
202                    send(stream, &ArtifactMsg::Reject { key, reason }).await
203                }
204            },
205            ArtifactMsg::Announce { id, have } => {
206                self.note_peer_have(id, peer, have);
207                Ok(())
208            }
209            ArtifactMsg::Request { id, pieces } => {
210                let key = ArtifactKey::Root(id);
211                let Some(d) = self.servable(&key, who) else {
212                    let reason = "not available or not servable".to_string();
213                    return send(stream, &ArtifactMsg::Reject { key, reason }).await;
214                };
215                if let Some(cache) = self.swarm.cache() {
216                    cache.touch(&id);
217                }
218                for index in pieces {
219                    let Ok(data) = self.load_piece(&d, index) else {
220                        send(stream, &ArtifactMsg::NoPiece { id, index }).await?;
221                        continue;
222                    };
223                    self.chain_serve_once(&d, peer);
224                    for (n, block) in data.chunks(self.config().block_size).enumerate() {
225                        let offset = (n * self.config().block_size) as u64;
226                        let data = block.to_vec();
227                        self.swarm.bandwidth.upload(data.len()).await;
228                        send(
229                            stream,
230                            &ArtifactMsg::Piece {
231                                id,
232                                index,
233                                offset,
234                                data,
235                            },
236                        )
237                        .await?;
238                    }
239                    stats.pieces_served.push(index);
240                    stats.bytes_served += data.len() as u64;
241                }
242                Ok(())
243            }
244            other => Err(PeerError::Protocol(format!(
245                "unexpected {:?} frame from fetcher",
246                other.frame_type()
247            ))),
248        }
249    }
250
251    fn servable(&self, key: &ArtifactKey, who: &ServePeer) -> Option<ArtifactDescriptor> {
252        self.resolve(key)
253            .filter(|d| self.is_servable_to(d, &Audience::Serve(who)))
254    }
255
256    /// Fetch `key` from `peers` with the v1 [`SequentialScheduler`].
257    pub async fn fetch(
258        &self,
259        peers: &mut PeerSet,
260        key: ArtifactKey,
261    ) -> Result<FetchOutcome, FetchError> {
262        self.fetch_with(peers, key, &mut SequentialScheduler).await
263    }
264
265    /// Fetch `key`, trying each live peer in turn until every piece is
266    /// held and the whole content verifies. Pieces already held are never
267    /// requested, so a repeated call resumes an interrupted transfer. A
268    /// peer whose pieces do not assemble to its declared content is dropped
269    /// and its descriptor discarded. The outcome (verified or failed) is
270    /// chained as `artifact.fetch`.
271    pub async fn fetch_with(
272        &self,
273        peers: &mut PeerSet,
274        key: ArtifactKey,
275        scheduler: &mut dyn PieceScheduler,
276    ) -> Result<FetchOutcome, FetchError> {
277        let mut progress = Progress::default();
278        let mut last_error = String::from("no peers");
279        let mut last_peer = String::new();
280        let mut verify_failed = false;
281        let mut done = self.resolve(&key).filter(|d| self.is_complete(d));
282        for link in peers.links.iter_mut().filter(|l| !l.dead) {
283            if done.is_some() {
284                break;
285            }
286            last_peer = link.peer_id.clone();
287            let before = (progress.pieces, progress.bytes);
288            let err = match self.fetch_from(link, key, scheduler, &mut progress).await {
289                Ok(d) if self.have_of(&d).is_complete() => {
290                    match self.promote(&d, &progress.written) {
291                        Ok(()) => {
292                            done = Some(d);
293                            continue;
294                        }
295                        Err(e) => {
296                            // The liar's pieces were discarded: do not count them.
297                            (progress.pieces, progress.bytes) = before;
298                            progress.sources.retain(|p| *p != link.peer_id);
299                            progress.current = None;
300                            progress.bad_descriptors.push((
301                                link.peer_id.clone(),
302                                d.id().to_string(),
303                                e.to_string(),
304                            ));
305                            verify_failed = true;
306                            PeerError::Local(e)
307                        }
308                    }
309                }
310                Ok(_) => {
311                    verify_failed = false;
312                    continue; // this peer has no more of what is missing
313                }
314                Err(e) => {
315                    verify_failed = false;
316                    e
317                }
318            };
319            last_error = err.to_string();
320            if !matches!(err, PeerError::Refused(_)) {
321                link.dead = true;
322                let _ = link.stream.close().await;
323            }
324        }
325        let Some(d) = done else {
326            let missing = progress
327                .current
328                .as_ref()
329                .map_or(0, |d| d.piece_count() - self.have_of(d).count());
330            let err = self.fetch_failed(&key, &progress, &last_peer, last_error, missing);
331            return Err(match err {
332                FetchError::Incomplete { last_error, .. } if verify_failed => {
333                    FetchError::Verification(last_error)
334                }
335                other => other,
336            });
337        };
338        let id = d.id();
339        self.materialize(&id)?;
340        self.chain_fetch(serde_json::json!({
341            "artifact_id": id.to_string(),
342            "content_hash": d.content_hex(),
343            "source_peer": progress.sources.last().cloned().unwrap_or_default(),
344            "sources": progress.sources,
345            "bytes": progress.bytes,
346            "total_size": d.total_size,
347            "pieces_fetched": progress.pieces,
348            "pieces_rejected": progress.rejected,
349            "descriptors_rejected": rejected_json(&progress),
350            "result": "verified",
351            "node": self.node_id(),
352        }));
353        Ok(FetchOutcome {
354            id,
355            descriptor: d,
356            pieces_fetched: progress.pieces,
357            bytes_fetched: progress.bytes,
358            pieces_rejected: progress.rejected,
359            sources: progress.sources,
360        })
361    }
362
363    pub(crate) fn is_complete(&self, d: &ArtifactDescriptor) -> bool {
364        self.have(&d.id()).is_some_and(|h| h.is_complete())
365    }
366
367    fn fetch_failed(
368        &self,
369        key: &ArtifactKey,
370        progress: &Progress,
371        last_peer: &str,
372        last_error: String,
373        missing: u32,
374    ) -> FetchError {
375        self.chain_fetch(serde_json::json!({
376            "key": key.to_string(),
377            "artifact_id": progress.current.as_ref().map(|d| d.id().to_string()),
378            "source_peer": last_peer,
379            "sources": progress.sources,
380            "bytes": progress.bytes,
381            "pieces_fetched": progress.pieces,
382            "pieces_rejected": progress.rejected,
383            "missing_pieces": missing,
384            "descriptors_rejected": rejected_json(progress),
385            "result": "failed",
386            "error": last_error,
387            "node": self.node_id(),
388        }));
389        FetchError::Incomplete {
390            missing,
391            last_error,
392        }
393    }
394
395    async fn fetch_from(
396        &self,
397        link: &mut PeerLink,
398        key: ArtifactKey,
399        scheduler: &mut dyn PieceScheduler,
400        progress: &mut Progress,
401    ) -> Result<ArtifactDescriptor, PeerError> {
402        let stream = link.stream.as_mut();
403        let peer = link.peer_id.clone();
404        send(stream, &ArtifactMsg::MetaRequest { key }).await?;
405        let d = match self.recv(stream).await? {
406            ArtifactMsg::Meta { descriptor } => descriptor,
407            ArtifactMsg::Reject { reason, .. } => return Err(PeerError::Refused(reason)),
408            other => return Err(unexpected(&other)),
409        };
410        let matches_key = match key {
411            ArtifactKey::Root(id) => d.id() == id,
412            ArtifactKey::Content(h) => d.content_hash == h,
413        };
414        if !matches_key {
415            return Err(PeerError::Protocol(format!(
416                "descriptor does not match {key}"
417            )));
418        }
419        self.check_descriptor(&d, &crate::mesh_swarm_fetch::Expect::default())?;
420        // Pending only: the content hash is the peer's claim until the
421        // assembled pieces prove it (see `promote`).
422        let id = self.note_pending(&d)?;
423        progress.current = Some(d.clone());
424        let mut peer_has = match self.recv(stream).await? {
425            ArtifactMsg::Announce { id: aid, have }
426                if aid == id && have.len() == d.piece_count() =>
427            {
428                have
429            }
430            other => return Err(unexpected(&other)),
431        };
432        let mine = self.have_of(&d);
433        send(stream, &ArtifactMsg::Announce { id, have: mine }).await?;
434
435        let mut strikes: BTreeMap<u32, u32> = BTreeMap::new();
436        loop {
437            let mut need = self.have_of(&d);
438            for (&i, &n) in &strikes {
439                if n >= self.config().max_piece_retries {
440                    need.set(i, true); // given up on this piece from this peer
441                }
442            }
443            let batch = scheduler.next_batch(&need, &peer_has, self.config().request_window);
444            if batch.is_empty() {
445                return Ok(d);
446            }
447            send(
448                stream,
449                &ArtifactMsg::Request {
450                    id,
451                    pieces: batch.clone(),
452                },
453            )
454            .await?;
455            for index in batch {
456                match self.recv_piece(stream, &d, index).await? {
457                    Some(data) => {
458                        let hash = *blake3::hash(&data).as_bytes();
459                        if hash != d.pieces[index as usize] {
460                            progress.rejected += 1;
461                            *strikes.entry(index).or_default() += 1;
462                            self.chain_piece_rejected(&id, index, &peer, "hash mismatch");
463                            continue;
464                        }
465                        if self.store_piece(&data, &hash)? {
466                            progress.written.insert(hash);
467                        }
468                        progress.pieces += 1;
469                        progress.bytes += data.len() as u64;
470                        if !progress.sources.contains(&peer) {
471                            progress.sources.push(peer.clone());
472                        }
473                    }
474                    None => peer_has.set(index, false),
475                }
476            }
477        }
478    }
479
480    /// Receive all blocks of piece `index` (in offset order), or `None` if
481    /// the peer answers `no_piece`.
482    async fn recv_piece(
483        &self,
484        stream: &mut dyn MeshStream,
485        d: &ArtifactDescriptor,
486        index: u32,
487    ) -> Result<Option<Vec<u8>>, PeerError> {
488        let want = d.piece_len(index) as usize;
489        let mut buf: Vec<u8> = Vec::new();
490        while buf.len() < want {
491            match self.recv(stream).await? {
492                ArtifactMsg::Piece {
493                    id,
494                    index: i,
495                    offset,
496                    data,
497                } if id == d.id() && i == index && offset == buf.len() as u64 => {
498                    if buf.len() + data.len() > want {
499                        return Err(PeerError::Protocol("piece longer than descriptor".into()));
500                    }
501                    // Grow with the bytes actually received: a descriptor
502                    // claiming a huge piece cannot make us pre-allocate it.
503                    buf.extend_from_slice(&data);
504                }
505                ArtifactMsg::NoPiece { id, index: i } if id == d.id() && i == index => {
506                    return Ok(None);
507                }
508                other => return Err(unexpected(&other)),
509            }
510        }
511        Ok(Some(buf))
512    }
513}
514
515fn rejected_json(progress: &Progress) -> serde_json::Value {
516    progress
517        .bad_descriptors
518        .iter()
519        .map(|(peer, id, reason)| {
520            serde_json::json!({ "peer": peer, "artifact_id": id, "reason": reason })
521        })
522        .collect()
523}
524
525fn unexpected(msg: &ArtifactMsg) -> PeerError {
526    let what = match msg {
527        ArtifactMsg::Reject { reason, .. } => return PeerError::Refused(reason.clone()),
528        ArtifactMsg::MetaRequest { .. } => "meta_request",
529        ArtifactMsg::Announce { .. } => "announce",
530        ArtifactMsg::Request { .. } => "request",
531        ArtifactMsg::Meta { .. } => "meta",
532        ArtifactMsg::Piece { .. } => "piece",
533        ArtifactMsg::NoPiece { .. } => "no_piece",
534    };
535    PeerError::Protocol(format!("unexpected {what} message"))
536}