1use std::time::Duration;
6
7use crate::mesh::MeshError;
8use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
9use crate::workload_pkg::verify::MAX_FILE_BYTES;
10
11pub const DEFAULT_PIECE_SIZE: u64 = 16 * 1024 * 1024;
13pub const DEFAULT_MAX_PIECE_SIZE: u64 = 16 * 1024 * 1024;
16pub const DEFAULT_MAX_ARTIFACT_BYTES: u64 = 64 * 1024 * 1024 * 1024;
19pub const MIN_PIECE_SIZE: u64 = 1024;
21pub const MAX_PIECE_SIZE: u64 = 1024 * 1024 * 1024;
23pub const DEFAULT_BLOCK_SIZE: usize = 1024 * 1024;
25pub const MAX_ARTIFACT_FRAME: usize = 2 * 1024 * 1024;
27pub const MAX_REQUEST_PIECES: usize = 1024;
29pub const MAX_REASON_BYTES: usize = 256;
31
32const ROOT_DOMAIN: &[u8] = b"weftos.artifact.v1\0";
33
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum WireError {
37 #[error("artifact frame too large: {size} bytes (max {max})")]
39 FrameTooLarge {
40 size: usize,
42 max: usize,
44 },
45 #[error("malformed artifact frame: {0}")]
47 Malformed(String),
48}
49
50impl From<WireError> for MeshError {
51 fn from(e: WireError) -> Self {
52 match e {
53 WireError::FrameTooLarge { size, max } => MeshError::MessageTooLarge { size, max },
54 WireError::Malformed(m) => MeshError::Transport(m),
55 }
56 }
57}
58
59pub(crate) fn bad(msg: impl Into<String>) -> WireError {
60 WireError::Malformed(msg.into())
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
66pub struct ArtifactId(pub [u8; 32]);
67
68impl ArtifactId {
69 pub fn from_hex(s: &str) -> Option<Self> {
71 hex_decode_exact::<32>(s).map(Self)
72 }
73}
74
75impl std::fmt::Display for ArtifactId {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.write_str(&hex_encode(&self.0))
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum ArtifactKey {
84 Root(ArtifactId),
86 Content([u8; 32]),
88}
89
90impl std::fmt::Display for ArtifactKey {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::Root(id) => write!(f, "root:{id}"),
94 Self::Content(h) => write!(f, "content:{}", hex_encode(h)),
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Bitfield {
102 pub(crate) len: u32,
103 pub(crate) bits: Vec<u8>,
104}
105
106impl Bitfield {
107 pub fn new(len: u32) -> Self {
109 Self {
110 len,
111 bits: vec![0; (len as usize).div_ceil(8)],
112 }
113 }
114
115 pub fn len(&self) -> u32 {
117 self.len
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.len == 0
123 }
124
125 pub fn get(&self, i: u32) -> bool {
127 i < self.len && self.bits[(i / 8) as usize] & (0x80 >> (i % 8)) != 0
128 }
129
130 pub fn set(&mut self, i: u32, v: bool) {
132 if i >= self.len {
133 return;
134 }
135 let mask = 0x80 >> (i % 8);
136 let byte = &mut self.bits[(i / 8) as usize];
137 if v {
138 *byte |= mask;
139 } else {
140 *byte &= !mask;
141 }
142 }
143
144 pub fn count(&self) -> u32 {
146 (0..self.len).filter(|&i| self.get(i)).count() as u32
147 }
148
149 pub fn is_complete(&self) -> bool {
151 self.count() == self.len
152 }
153
154 pub fn missing(&self) -> impl Iterator<Item = u32> + '_ {
156 (0..self.len).filter(|&i| !self.get(i))
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct ArtifactDescriptor {
163 pub piece_size: u64,
165 pub total_size: u64,
167 pub content_hash: [u8; 32],
169 pub pieces: Vec<[u8; 32]>,
171}
172
173impl ArtifactDescriptor {
174 pub fn id(&self) -> ArtifactId {
179 let mut h = blake3::Hasher::new();
180 h.update(ROOT_DOMAIN);
181 h.update(&self.piece_size.to_be_bytes());
182 h.update(&self.total_size.to_be_bytes());
183 h.update(&self.content_hash);
184 for p in &self.pieces {
185 h.update(p);
186 }
187 ArtifactId(*h.finalize().as_bytes())
188 }
189
190 pub fn piece_count(&self) -> u32 {
192 self.pieces.len() as u32
193 }
194
195 pub fn piece_len(&self, i: u32) -> u64 {
197 let start = u64::from(i) * self.piece_size;
198 self.total_size.saturating_sub(start).min(self.piece_size)
199 }
200
201 pub fn content_hex(&self) -> String {
203 hex_encode(&self.content_hash)
204 }
205
206 pub fn validate(&self) -> Result<(), WireError> {
208 if !(MIN_PIECE_SIZE..=MAX_PIECE_SIZE).contains(&self.piece_size) {
209 return Err(bad(format!("piece size {} out of range", self.piece_size)));
210 }
211 let expected = self.total_size.div_ceil(self.piece_size);
212 if expected != self.pieces.len() as u64 || expected > u64::from(u32::MAX) {
213 return Err(bad(format!(
214 "{} pieces listed for {} bytes",
215 self.pieces.len(),
216 self.total_size
217 )));
218 }
219 Ok(())
220 }
221}
222
223const PIECE_HEADER_BYTES: usize = 1 + 32 + 4 + 8 + 4;
225
226#[derive(Debug, Clone)]
228pub struct ExchangeConfig {
229 pub piece_size: u64,
231 pub block_size: usize,
233 pub request_window: usize,
235 pub max_piece_retries: u32,
238 pub materialize_limit: u64,
241 pub recv_timeout: Duration,
243 pub serve_idle_timeout: Duration,
246 pub upload_bytes_per_sec: Option<u64>,
248 pub download_bytes_per_sec: Option<u64>,
250 pub max_sources: usize,
252 pub ban_after_corrupt: u32,
254 pub max_piece_size: u64,
257 pub max_artifact_bytes: u64,
260 pub redistribution: std::sync::Arc<dyn crate::mesh_swarm_state::RedistributionPolicy>,
263}
264
265impl Default for ExchangeConfig {
266 fn default() -> Self {
267 Self {
268 piece_size: DEFAULT_PIECE_SIZE,
269 block_size: DEFAULT_BLOCK_SIZE,
270 request_window: 4,
271 max_piece_retries: 3,
272 materialize_limit: MAX_FILE_BYTES,
273 recv_timeout: Duration::from_secs(30),
274 serve_idle_timeout: Duration::from_secs(120),
275 upload_bytes_per_sec: None,
276 download_bytes_per_sec: None,
277 max_sources: 4,
278 ban_after_corrupt: 1,
279 max_piece_size: DEFAULT_MAX_PIECE_SIZE,
280 max_artifact_bytes: DEFAULT_MAX_ARTIFACT_BYTES,
281 redistribution: std::sync::Arc::new(crate::mesh_swarm_state::ManifestPolicy),
282 }
283 }
284}
285
286impl ExchangeConfig {
287 pub fn validate(&self) -> Result<(), ExchangeError> {
289 if !(MIN_PIECE_SIZE..=MAX_PIECE_SIZE).contains(&self.piece_size) {
290 return Err(ExchangeError::Config("piece_size out of range".into()));
291 }
292 if self.block_size == 0 || self.block_size + PIECE_HEADER_BYTES > MAX_ARTIFACT_FRAME {
293 return Err(ExchangeError::Config(
294 "block_size must fit one artifact frame".into(),
295 ));
296 }
297 if self.request_window == 0 || self.max_piece_retries == 0 {
298 return Err(ExchangeError::Config(
299 "request_window and max_piece_retries must be > 0".into(),
300 ));
301 }
302 if self.recv_timeout.is_zero() || self.serve_idle_timeout.is_zero() {
303 return Err(ExchangeError::Config("timeouts must be > 0".into()));
304 }
305 if self.upload_bytes_per_sec == Some(0) || self.download_bytes_per_sec == Some(0) {
306 return Err(ExchangeError::Config("bandwidth caps must be > 0".into()));
307 }
308 if self.piece_size > self.max_piece_size || self.max_piece_size > MAX_PIECE_SIZE {
309 return Err(ExchangeError::Config(
310 "piece_size must not exceed max_piece_size (and max_piece_size the protocol limit)"
311 .into(),
312 ));
313 }
314 if self.max_artifact_bytes == 0 {
315 return Err(ExchangeError::Config("max_artifact_bytes must be > 0".into()));
316 }
317 if self.max_sources == 0 || self.ban_after_corrupt == 0 {
318 return Err(ExchangeError::Config(
319 "max_sources and ban_after_corrupt must be > 0".into(),
320 ));
321 }
322 Ok(())
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
328pub enum ExchangeError {
329 #[error("artifact exchange config: {0}")]
331 Config(String),
332 #[error(transparent)]
334 Wire(#[from] WireError),
335 #[error("artifact storage: {0}")]
337 Store(String),
338 #[error("artifact source io: {0}")]
340 Io(String),
341 #[error("artifact content mismatch: {0}")]
343 Mismatch(String),
344 #[error("unknown artifact {0}")]
346 Unknown(String),
347 #[error("peer {0} is banned")]
349 Banned(String),
350 #[error("revoked: {0}")]
352 Revoked(String),
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn id_binds_piece_list_sizes_and_content_hash() {
361 let d = ArtifactDescriptor {
362 piece_size: 1024,
363 total_size: 1500,
364 content_hash: [0; 32],
365 pieces: vec![[1; 32], [2; 32]],
366 };
367 let mut swapped = d.clone();
368 swapped.pieces.swap(0, 1);
369 let mut shorter = d.clone();
370 shorter.total_size = 1400;
371 let mut other_claim = d.clone();
372 other_claim.content_hash = [9; 32];
373 assert_ne!(d.id(), swapped.id());
374 assert_ne!(d.id(), shorter.id());
375 assert_ne!(d.id(), other_claim.id(), "id binds the content hash");
376 assert_eq!(d.piece_len(0), 1024);
377 assert_eq!(d.piece_len(1), 476);
378 assert_eq!(ArtifactId::from_hex(&d.id().to_string()), Some(d.id()));
379 }
380
381 #[test]
382 fn bitfield_tracks_missing_pieces() {
383 let mut b = Bitfield::new(10);
384 for i in [0, 3, 9] {
385 b.set(i, true);
386 }
387 assert_eq!(b.count(), 3);
388 assert!(!b.is_complete());
389 assert_eq!(b.missing().collect::<Vec<_>>(), vec![1, 2, 4, 5, 6, 7, 8]);
390 b.set(42, true); assert!(!b.get(42));
392 }
393}