Skip to main content

clawft_kernel/
mesh_artifact.rs

1//! Artifact exchange over the mesh (ADR-099 section 6, mesh-placement-11).
2//!
3//! [`ArtifactExchange`] is the node-local side of the swarm-ready piece
4//! protocol defined in [`crate::mesh_artifact_wire`]. It keeps
5//! descriptors, derives each artifact's `have` bitfield from the pieces
6//! held in [`ArtifactStore`], and decides what may be served.
7//!
8//! - **Identity.** An artifact is its piece-list root
9//!   ([`ArtifactDescriptor::id`]). Pieces are stored in `ArtifactStore`
10//!   under their own BLAKE3, so a node that holds a verified artifact can
11//!   serve it (origin or not), and a restart resumes from the pieces
12//!   already on disk.
13//! - **Trust.** A descriptor received from a peer is only *pending*: its
14//!   pieces are hash-checked against it, but its `content_hash` is the
15//!   peer's claim. It becomes *verified* only when the pieces assemble to
16//!   that hash on this node ([`Self::promote`]); if they do not, the
17//!   descriptor is discarded and the pieces that fetch wrote are rolled
18//!   back. Blobs already in the store are never removed on a peer's word. Content keys
19//!   ([`ArtifactKey::Content`]) resolve to verified descriptors only.
20//! - **Governance.** Only verified artifacts whose content a signed
21//!   manifest lists (and that manifest verified here) are served
22//!   ([`Self::is_servable`]); see [`crate::mesh_artifact_pkg`].
23//! - **Audit.** Outcomes are chained (`artifact.fetch`,
24//!   `artifact.piece_rejected`, `artifact.serve` once per peer);
25//!   individual pieces are not.
26//!
27//! Transfer (serve / fetch) lives in [`crate::mesh_artifact_transfer`].
28//! v1 fetches from one peer at a time; card 25 adds multi-source fetch,
29//! rarest-first, seeding policy, caching and bandwidth limits on top of
30//! the same types.
31
32use std::collections::HashSet;
33use std::io::Read;
34use std::sync::Arc;
35
36use dashmap::DashMap;
37
38use crate::artifact_store::{ArtifactStore, ArtifactType};
39use crate::chain::ChainManager;
40pub use crate::mesh_artifact_types::{ExchangeConfig, ExchangeError};
41use crate::mesh_artifact_wire::{ArtifactDescriptor, ArtifactId, ArtifactKey, Bitfield};
42use crate::mesh_swarm_state::{Audience, GrantInfo, GrantOrigin, SwarmState};
43use crate::workload_pkg::codec::hex_encode;
44
45/// Most partially downloaded artifacts held at once.
46pub const MAX_PENDING: usize = 64;
47
48/// Node-local state of the artifact piece protocol.
49pub struct ArtifactExchange {
50    node_id: String,
51    pub(crate) store: Arc<ArtifactStore>,
52    config: ExchangeConfig,
53    /// Verified descriptors: pieces assemble to `content_hash`.
54    pub(crate) descriptors: DashMap<ArtifactId, ArtifactDescriptor>,
55    /// Descriptors received from peers, not yet verified (resume state).
56    pub(crate) pending: DashMap<ArtifactId, ArtifactDescriptor>,
57    /// Content hash -> id, verified descriptors only.
58    pub(crate) by_content: DashMap<[u8; 32], ArtifactId>,
59    /// Content hash -> the verified signed manifests that list it (one per package).
60    pub(crate) grants: DashMap<[u8; 32], Vec<GrantInfo>>,
61    /// `(artifact, peer)` pairs already chained as `artifact.serve`.
62    served: DashMap<(ArtifactId, String), ()>,
63    /// Last `have` each peer announced (input for card 25's scheduler).
64    pub(crate) peer_haves: DashMap<(ArtifactId, String), Bitfield>,
65    pub(crate) chain: Option<Arc<ChainManager>>,
66    /// Swarm state: bandwidth caps, bans, link stats, revocations, cache hook.
67    pub(crate) swarm: SwarmState,
68}
69
70impl ArtifactExchange {
71    /// Exchange for `node_id` over `store`.
72    pub fn new(
73        node_id: impl Into<String>,
74        store: Arc<ArtifactStore>,
75        config: ExchangeConfig,
76    ) -> Result<Self, ExchangeError> {
77        config.validate()?;
78        Ok(Self {
79            node_id: node_id.into(),
80            store,
81            swarm: SwarmState::new(&config),
82            config,
83            descriptors: DashMap::new(),
84            pending: DashMap::new(),
85            by_content: DashMap::new(),
86            grants: DashMap::new(),
87            served: DashMap::new(),
88            peer_haves: DashMap::new(),
89            chain: None,
90        })
91    }
92
93    /// Chain transfer outcomes to `cm`.
94    pub fn set_chain_manager(&mut self, cm: Arc<ChainManager>) {
95        self.chain = Some(cm);
96    }
97
98    /// This node's id.
99    pub fn node_id(&self) -> &str {
100        &self.node_id
101    }
102
103    /// Backing store.
104    pub fn store(&self) -> &Arc<ArtifactStore> {
105        &self.store
106    }
107
108    /// Configuration.
109    pub fn config(&self) -> &ExchangeConfig {
110        &self.config
111    }
112
113    /// Split `reader` into pieces, store each, and register the artifact.
114    /// Streams: at most one piece is in memory. When `expect` is given
115    /// (`content hash, size`), a mismatch is an error and nothing is
116    /// registered.
117    pub fn seed_reader(
118        &self,
119        reader: &mut dyn Read,
120        expect: Option<([u8; 32], u64)>,
121    ) -> Result<ArtifactDescriptor, ExchangeError> {
122        let piece_size = self.config.piece_size;
123        let mut whole = blake3::Hasher::new();
124        let mut pieces = Vec::new();
125        let mut total = 0u64;
126        let mut buf = vec![0u8; piece_size as usize];
127        loop {
128            let n = read_full(reader, &mut buf)?;
129            if n == 0 {
130                break;
131            }
132            let piece = &buf[..n];
133            whole.update(piece);
134            let hash = *blake3::hash(piece).as_bytes();
135            self.store_piece(piece, &hash)?;
136            pieces.push(hash);
137            total += n as u64;
138            if n < buf.len() {
139                break;
140            }
141        }
142        let descriptor = ArtifactDescriptor {
143            piece_size,
144            total_size: total,
145            content_hash: *whole.finalize().as_bytes(),
146            pieces,
147        };
148        if let Some((hash, size)) = expect
149            && (hash != descriptor.content_hash || size != total)
150        {
151            return Err(ExchangeError::Mismatch(format!(
152                "expected {} ({size} bytes), read {} ({total} bytes)",
153                hex_encode(&hash),
154                descriptor.content_hex()
155            )));
156        }
157        // Built from the bytes just read: verified by construction.
158        self.register_verified(descriptor.clone())?;
159        Ok(descriptor)
160    }
161
162    /// [`Self::seed_reader`] over an in-memory buffer.
163    pub fn seed_bytes(&self, content: &[u8]) -> Result<ArtifactDescriptor, ExchangeError> {
164        self.seed_reader(&mut &content[..], None)
165    }
166
167    /// Record a descriptor whose pieces are known to assemble to its
168    /// `content_hash`.
169    fn register_verified(&self, d: ArtifactDescriptor) -> Result<ArtifactId, ExchangeError> {
170        d.validate()?;
171        let id = d.id();
172        self.pending.remove(&id);
173        let content_hash = d.content_hash;
174        self.by_content.insert(content_hash, id);
175        self.descriptors.insert(id, d);
176        self.on_verified(&id, &content_hash);
177        Ok(id)
178    }
179
180    /// Remember a peer's descriptor as pending (resume state). It is not
181    /// resolvable by content and never served until [`Self::promote`]d.
182    pub(crate) fn note_pending(&self, d: &ArtifactDescriptor) -> Result<ArtifactId, ExchangeError> {
183        d.validate()?;
184        let id = d.id();
185        if !self.descriptors.contains_key(&id) {
186            if !self.pending.contains_key(&id) && self.pending.len() >= MAX_PENDING {
187                return Err(ExchangeError::Config(format!(
188                    "{MAX_PENDING} partial downloads already held; forget one first"
189                )));
190            }
191            self.pending.insert(id, d.clone());
192            if let Some(cache) = self.swarm.cache() {
193                cache.on_pending();
194            }
195        }
196        Ok(id)
197    }
198
199    /// Check that `d`'s pieces (all held) assemble to its `content_hash`.
200    /// On success `d` becomes verified. On failure `d` is discarded along
201    /// with the pieces in `written` (blobs this fetch created) that no
202    /// other known artifact uses, and the error returned.
203    pub(crate) fn promote(
204        &self,
205        d: &ArtifactDescriptor,
206        written: &HashSet<[u8; 32]>,
207    ) -> Result<(), ExchangeError> {
208        match self.check_whole(d, &mut |_| Ok(())) {
209            Ok(_) => self.register_verified(d.clone()).map(|_| ()),
210            Err(e) => {
211                self.discard(d, written);
212                Err(e)
213            }
214        }
215    }
216
217    /// Forget a descriptor that failed verification and roll back the
218    /// pieces this fetch wrote for it.
219    ///
220    /// Only blobs in `written` are candidates: the descriptor came from an
221    /// unauthenticated peer, so the piece hashes it lists say nothing about
222    /// who owns blobs that were already in the store (other subsystems'
223    /// data, pieces from before a restart, honest resume state). Of those
224    /// candidates, pieces another verified or pending artifact lists are
225    /// kept.
226    fn discard(&self, d: &ArtifactDescriptor, written: &HashSet<[u8; 32]>) {
227        let id = d.id();
228        if self.descriptors.get(&id).is_some_and(|v| *v == *d) {
229            return; // never discard verified state
230        }
231        self.pending.remove_if(&id, |_, p| p == d);
232        let mut keep = HashSet::new();
233        for v in self.descriptors.iter() {
234            keep.insert(v.content_hash);
235            keep.extend(v.pieces.iter().copied());
236        }
237        for p in self.pending.iter() {
238            keep.extend(p.pieces.iter().copied());
239        }
240        for piece in d.pieces.iter().filter(|p| written.contains(*p)) {
241            if !keep.contains(piece) {
242                let _ = self.store.remove(&hex_encode(piece));
243            }
244        }
245    }
246
247    /// Verified descriptor by id.
248    pub fn descriptor(&self, id: &ArtifactId) -> Option<ArtifactDescriptor> {
249        self.descriptors.get(id).map(|d| d.clone())
250    }
251
252    /// True when `id` is verified on this node.
253    pub fn is_verified(&self, id: &ArtifactId) -> bool {
254        self.descriptors.contains_key(id)
255    }
256
257    /// Verified descriptor by key.
258    pub fn resolve(&self, key: &ArtifactKey) -> Option<ArtifactDescriptor> {
259        let id = match key {
260            ArtifactKey::Root(id) => *id,
261            ArtifactKey::Content(h) => *self.by_content.get(h)?,
262        };
263        self.descriptor(&id)
264    }
265
266    /// Pieces held locally for a verified or pending artifact, derived
267    /// from the store.
268    pub fn have(&self, id: &ArtifactId) -> Option<Bitfield> {
269        let d = self
270            .descriptor(id)
271            .or_else(|| self.pending.get(id).map(|d| d.clone()))?;
272        Some(self.have_of(&d))
273    }
274
275    /// Pieces of `d` held in the store.
276    pub(crate) fn have_of(&self, d: &ArtifactDescriptor) -> Bitfield {
277        let mut b = Bitfield::new(d.piece_count());
278        for (i, p) in d.pieces.iter().enumerate() {
279            b.set(i as u32, self.store.contains(&hex_encode(p)));
280        }
281        b
282    }
283
284    /// Stream the whole artifact through `sink` in piece order, checking
285    /// every piece and the whole-content hash. Returns the byte count.
286    pub fn read_to(
287        &self,
288        id: &ArtifactId,
289        sink: &mut dyn FnMut(&[u8]) -> Result<(), ExchangeError>,
290    ) -> Result<u64, ExchangeError> {
291        let d = self
292            .descriptor(id)
293            .ok_or_else(|| ExchangeError::Unknown(id.to_string()))?;
294        self.check_whole(&d, sink)
295    }
296
297    fn check_whole(
298        &self,
299        d: &ArtifactDescriptor,
300        sink: &mut dyn FnMut(&[u8]) -> Result<(), ExchangeError>,
301    ) -> Result<u64, ExchangeError> {
302        let id = d.id();
303        let mut whole = blake3::Hasher::new();
304        let mut total = 0u64;
305        for i in 0..d.piece_count() {
306            let data = self.load_piece(d, i)?;
307            whole.update(&data);
308            total += data.len() as u64;
309            sink(&data)?;
310        }
311        if *whole.finalize().as_bytes() != d.content_hash || total != d.total_size {
312            return Err(ExchangeError::Mismatch(format!(
313                "pieces of {id} do not assemble to content {}",
314                d.content_hex()
315            )));
316        }
317        Ok(total)
318    }
319
320    /// Whole artifact in memory (bounded by `materialize_limit`).
321    pub fn read_all(&self, id: &ArtifactId) -> Result<Vec<u8>, ExchangeError> {
322        let d = self
323            .descriptor(id)
324            .ok_or_else(|| ExchangeError::Unknown(id.to_string()))?;
325        if d.total_size > self.config.materialize_limit {
326            return Err(ExchangeError::Config(format!(
327                "artifact {id} is larger than materialize_limit"
328            )));
329        }
330        let mut out = Vec::with_capacity(d.total_size as usize);
331        self.read_to(id, &mut |b| {
332            out.extend_from_slice(b);
333            Ok(())
334        })?;
335        Ok(out)
336    }
337
338    /// Allow serving `content_hash` under a verified package.
339    #[cfg(test)]
340    pub(crate) fn grant(&self, content_hash: [u8; 32], package_id: &str) {
341        self.grant_with(content_hash, package_id, Vec::new(), GrantOrigin::OptIn);
342    }
343
344    /// [`Self::grant`], recording the hex public keys of the package's
345    /// signers (a signer revocation names the key) and the package's licence
346    /// standing. Grants are kept per package: a second package
347    /// that lists the same content does not replace the first.
348    pub(crate) fn grant_with(
349        &self,
350        content_hash: [u8; 32],
351        package_id: &str,
352        signers: Vec<String>,
353        origin: GrantOrigin,
354    ) {
355        if self.is_revoked_subject(package_id, &signers, &content_hash) {
356            return; // a revoked package, signer or hash is never allowed to seed
357        }
358        let info = GrantInfo {
359            package_id: package_id.to_string(),
360            signers,
361            origin,
362        };
363        {
364            let mut gs = self.grants.entry(content_hash).or_default();
365            match gs.iter_mut().find(|g| g.package_id == package_id) {
366                Some(g) => *g = info,
367                None => gs.push(info),
368            }
369        }
370        self.on_granted(&content_hash);
371    }
372
373    /// Governance: may `d` be listed in broadcast facts and seeded? It must
374    /// be verified on this node (its pieces assemble to its `content_hash`),
375    /// a signed manifest that verified here must list it, the redistribution
376    /// policy must allow it for [`Audience::Advertise`], and neither the
377    /// package, its signers nor the content hash may be revoked. Serving a
378    /// particular peer is [`Self::is_servable_to`].
379    pub fn is_servable(&self, d: &ArtifactDescriptor) -> bool {
380        self.is_servable_to(d, &Audience::Advertise)
381    }
382
383    /// [`Self::is_servable`] for a specific audience.
384    pub fn is_servable_to(&self, d: &ArtifactDescriptor, audience: &Audience<'_>) -> bool {
385        self.descriptors.get(&d.id()).is_some_and(|v| *v == *d)
386            && self.servable_grant(&d.content_hash, audience).is_some()
387            && !self.is_revoked(&d.content_hash)
388    }
389
390    /// Record the `have` a peer announced.
391    pub(crate) fn note_peer_have(&self, id: ArtifactId, peer: &str, have: Bitfield) {
392        self.peer_haves.insert((id, peer.to_string()), have);
393    }
394
395    /// Last `have` announced by `peer` for `id`.
396    pub fn peer_have(&self, id: &ArtifactId, peer: &str) -> Option<Bitfield> {
397        self.peer_haves
398            .get(&(*id, peer.to_string()))
399            .map(|b| b.clone())
400    }
401
402    /// Load piece `i` of `d` (the store re-checks its hash).
403    pub(crate) fn load_piece(
404        &self,
405        d: &ArtifactDescriptor,
406        i: u32,
407    ) -> Result<Vec<u8>, ExchangeError> {
408        let hash = d
409            .pieces
410            .get(i as usize)
411            .ok_or_else(|| ExchangeError::Unknown(format!("piece {i}")))?;
412        self.store
413            .load(&hex_encode(hash))
414            .map_err(|e| ExchangeError::Store(e.to_string()))
415    }
416
417    /// Store a piece whose hash the caller has already checked. Returns
418    /// `true` when this call created the blob, `false` when it was held.
419    pub(crate) fn store_piece(&self, data: &[u8], hash: &[u8; 32]) -> Result<bool, ExchangeError> {
420        let key = hex_encode(hash);
421        if self.store.contains(&key) {
422            return Ok(false);
423        }
424        let stored = self
425            .store
426            .store(data, ArtifactType::Generic)
427            .map_err(|e| ExchangeError::Store(e.to_string()))?;
428        if stored != key {
429            return Err(ExchangeError::Mismatch(format!(
430                "piece stored as {stored}, expected {key}"
431            )));
432        }
433        self.swarm.owned.insert(*hash, ());
434        Ok(true)
435    }
436
437    /// After a verified fetch, keep small artifacts whole as well.
438    pub(crate) fn materialize(&self, id: &ArtifactId) -> Result<(), ExchangeError> {
439        let Some(d) = self.descriptor(id) else {
440            return Ok(());
441        };
442        if d.total_size > self.config.materialize_limit || self.store.contains(&d.content_hex()) {
443            return Ok(());
444        }
445        let bytes = self.read_all(id)?;
446        self.store
447            .store(&bytes, ArtifactType::Generic)
448            .map_err(|e| ExchangeError::Store(e.to_string()))?;
449        self.swarm.owned.insert(d.content_hash, ());
450        Ok(())
451    }
452
453    // ── chain ────────────────────────────────────────────────────
454
455    pub(crate) fn chain_fetch(&self, payload: serde_json::Value) {
456        if let Some(cm) = &self.chain {
457            cm.append(
458                "mesh_artifact",
459                crate::chain::EVENT_KIND_ARTIFACT_FETCH,
460                Some(payload),
461            );
462        }
463    }
464
465    pub(crate) fn chain_piece_rejected(&self, id: &ArtifactId, index: u32, peer: &str, why: &str) {
466        if let Some(cm) = &self.chain {
467            cm.append(
468                "mesh_artifact",
469                crate::chain::EVENT_KIND_ARTIFACT_PIECE_REJECTED,
470                Some(serde_json::json!({
471                    "artifact_id": id.to_string(),
472                    "piece_index": index,
473                    "peer": peer,
474                    "reason": why,
475                    "node": self.node_id,
476                })),
477            );
478        }
479    }
480
481    /// Chain `artifact.serve` the first time `peer` is served `d`.
482    pub(crate) fn chain_serve_once(&self, d: &ArtifactDescriptor, peer: &str) {
483        let key = (d.id(), peer.to_string());
484        if self.served.insert(key, ()).is_some() {
485            return;
486        }
487        if let Some(cm) = &self.chain {
488            cm.append(
489                "mesh_artifact",
490                crate::chain::EVENT_KIND_ARTIFACT_SERVE,
491                Some(serde_json::json!({
492                    "artifact_id": d.id().to_string(),
493                    "content_hash": d.content_hex(),
494                    "peer": peer,
495                    "node": self.node_id,
496                })),
497            );
498        }
499    }
500}
501
502fn read_full(r: &mut dyn Read, buf: &mut [u8]) -> Result<usize, ExchangeError> {
503    let mut filled = 0;
504    while filled < buf.len() {
505        match r.read(&mut buf[filled..]) {
506            Ok(0) => break,
507            Ok(n) => filled += n,
508            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
509            Err(e) => return Err(ExchangeError::Io(e.to_string())),
510        }
511    }
512    Ok(filled)
513}
514
515#[cfg(test)]
516#[path = "mesh_artifact_unit_tests.rs"]
517mod tests;