1use std::{
2 collections::{BTreeMap, BTreeSet},
3 future::Future,
4 pin::Pin,
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicBool, AtomicU64, Ordering},
8 },
9 time::{Duration, Instant},
10};
11
12use chacha20poly1305::{
13 XChaCha20Poly1305, XNonce,
14 aead::{Aead as _, KeyInit as _, Payload},
15};
16use iroh_db_core::{BlobHash, DomainId};
17use iroh_db_security::DomainKey;
18use iroh_db_store::{BlobStore, BlobStoreError};
19use tokio::{
20 io::{AsyncRead, AsyncReadExt as _},
21 sync::Notify,
22};
23
24use crate::{
25 BlobError, BlobRef, BlobStream, IrohBlobStore,
26 model::{ChunkDescriptor, ManifestBody, ManifestEnvelope},
27 scheduler::{
28 BlobPriority, FetchScheduler, PrioritySignal, ProviderBook, ProviderMetrics, TransferPermit,
29 },
30 stream::validate_manifest,
31};
32
33pub(crate) const FORMAT_VERSION: u16 = 1;
34pub(crate) const MIN_CHUNK_SIZE: usize = 64 * 1024;
35pub(crate) const MAX_CHUNK_SIZE: usize = 8 * 1024 * 1024;
36pub(crate) const DEFAULT_CHUNK_SIZE: usize = 4 * 1024 * 1024;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct FetchOptions {
41 pub priority: BlobPriority,
42 pub prefetch_chunks: u8,
43}
44
45impl Default for FetchOptions {
46 fn default() -> Self {
47 Self {
48 priority: BlobPriority::Normal,
49 prefetch_chunks: 2,
50 }
51 }
52}
53
54#[derive(Default)]
55struct FetchCounters {
56 provider_attempts: AtomicU64,
57 chunks_fetched: AtomicU64,
58 bytes_fetched: AtomicU64,
59 retries: AtomicU64,
60 hedged_attempts: AtomicU64,
61 cache_hits: AtomicU64,
62}
63
64#[derive(Clone)]
66pub struct FetchMetrics {
67 counters: Arc<FetchCounters>,
68 providers: Arc<Mutex<ProviderBook>>,
69}
70
71impl Default for FetchMetrics {
72 fn default() -> Self {
73 Self::new(0)
74 }
75}
76
77impl FetchMetrics {
78 fn new(providers: usize) -> Self {
79 Self {
80 counters: Arc::new(FetchCounters::default()),
81 providers: Arc::new(Mutex::new(ProviderBook::new(providers))),
82 }
83 }
84
85 pub fn provider_attempts(&self) -> u64 {
86 self.counters.provider_attempts.load(Ordering::Relaxed)
87 }
88
89 pub fn chunks_fetched(&self) -> u64 {
90 self.counters.chunks_fetched.load(Ordering::Relaxed)
91 }
92
93 pub fn bytes_fetched(&self) -> u64 {
94 self.counters.bytes_fetched.load(Ordering::Relaxed)
95 }
96
97 pub fn retries(&self) -> u64 {
98 self.counters.retries.load(Ordering::Relaxed)
99 }
100
101 pub fn hedged_attempts(&self) -> u64 {
102 self.counters.hedged_attempts.load(Ordering::Relaxed)
103 }
104
105 pub fn cache_hits(&self) -> u64 {
106 self.counters.cache_hits.load(Ordering::Relaxed)
107 }
108
109 pub fn providers(&self) -> Vec<ProviderMetrics> {
110 self.providers
111 .lock()
112 .map_or_else(|_| Vec::new(), |book| book.snapshots(Instant::now()))
113 }
114}
115
116#[derive(Clone)]
117struct ProviderPool {
118 local: IrohBlobStore,
119 claims: Arc<dyn BlobStore>,
120 providers: Arc<Vec<iroh::endpoint::Connection>>,
121 scheduler: FetchScheduler,
122 available: Arc<Notify>,
123 in_flight: Arc<Mutex<BTreeMap<BlobHash, Arc<SharedFetch>>>>,
124 metrics: FetchMetrics,
125}
126
127impl ProviderPool {
128 async fn fetch(
129 &self,
130 hash: BlobHash,
131 expected_bytes: u64,
132 priority: BlobPriority,
133 allow_hedge: bool,
134 ) -> Result<(), BlobError> {
135 loop {
136 if self
137 .claims
138 .get(hash)
139 .await
140 .map_err(BlobError::from)?
141 .is_some()
142 {
143 self.metrics
144 .counters
145 .cache_hits
146 .fetch_add(1, Ordering::Relaxed);
147 return self.claims.claim(hash).await.map_err(BlobError::from);
148 }
149 let (shared, leader) = {
150 let mut in_flight = self
151 .in_flight
152 .lock()
153 .map_err(|_| BlobError::SchedulerUnavailable)?;
154 if let Some(shared) = in_flight.get(&hash) {
155 shared.promote(priority, allow_hedge);
156 (shared.clone(), false)
157 } else {
158 let shared = Arc::new(SharedFetch::new(priority, allow_hedge));
159 in_flight.insert(hash, shared.clone());
160 (shared, true)
161 }
162 };
163 if leader {
164 let owner = SharedFetchOwner::new(self.clone(), hash, shared.clone());
165 let result = async {
166 self.fetch_unclaimed_with_policy(hash, expected_bytes, &shared)
167 .await?;
168 self.claims.claim(hash).await.map_err(BlobError::from)
169 }
170 .await;
171 return owner.complete(result);
172 }
173 match shared.wait().await {
174 Err(BlobError::FetchCancelled) => {}
175 result => return result,
176 }
177 }
178 }
179
180 async fn fetch_unclaimed(
181 &self,
182 hash: BlobHash,
183 expected_bytes: u64,
184 priority: BlobPriority,
185 allow_hedge: bool,
186 ) -> Result<(), BlobError> {
187 let policy = Arc::new(SharedFetch::new(priority, allow_hedge));
188 self.fetch_unclaimed_with_policy(hash, expected_bytes, &policy)
189 .await
190 }
191
192 async fn fetch_unclaimed_with_policy(
193 &self,
194 hash: BlobHash,
195 expected_bytes: u64,
196 policy: &Arc<SharedFetch>,
197 ) -> Result<(), BlobError> {
198 if self.providers.is_empty() {
199 return Err(BlobError::NoProviders);
200 }
201 let mut excluded = BTreeSet::new();
202 let mut last_error = None;
203 while excluded.len() < self.providers.len() {
204 if !excluded.is_empty() {
205 self.metrics
206 .counters
207 .retries
208 .fetch_add(1, Ordering::Relaxed);
209 }
210 let attempt = self.reserve(&excluded, expected_bytes, policy).await?;
211 match self
212 .run_attempt(attempt, hash, expected_bytes, policy, &excluded)
213 .await?
214 {
215 AttemptResult::Success => {
216 self.record_success();
217 return Ok(());
218 }
219 AttemptResult::Failed(failures) => {
220 for (provider, error) in failures {
221 excluded.insert(provider);
222 last_error = Some(error);
223 }
224 }
225 }
226 }
227 Err(last_error.unwrap_or(BlobError::NoProviders))
228 }
229
230 async fn run_attempt(
231 &self,
232 attempt: RemoteAttempt,
233 hash: BlobHash,
234 expected_bytes: u64,
235 policy: &Arc<SharedFetch>,
236 excluded: &BTreeSet<usize>,
237 ) -> Result<AttemptResult, BlobError> {
238 let provider = attempt.provider;
239 if self.providers.len() - excluded.len() <= 1 {
240 return Ok(single_attempt_result(
241 provider,
242 self.execute(attempt, hash, expected_bytes).await,
243 ));
244 }
245 let mut primary = Box::pin(self.execute(attempt, hash, expected_bytes));
246 loop {
247 let promoted = policy.promoted.notified();
248 if policy.should_hedge() {
249 break;
250 }
251 tokio::select! {
252 result = &mut primary => return Ok(single_attempt_result(provider, result)),
253 () = promoted => {}
254 }
255 }
256 let delay = self.hedge_delay(provider)?;
257 tokio::select! {
258 result = &mut primary => return Ok(single_attempt_result(provider, result)),
259 () = tokio::time::sleep(delay) => {}
260 }
261
262 let mut hedge_excluded = excluded.clone();
263 hedge_excluded.insert(provider);
264 let mut reservation = Box::pin(self.reserve(&hedge_excluded, expected_bytes, policy));
265 let hedge = tokio::select! {
266 result = &mut primary => return Ok(single_attempt_result(provider, result)),
267 reservation = &mut reservation => reservation?,
268 };
269 let hedge_provider = hedge.provider;
270 self.metrics
271 .counters
272 .hedged_attempts
273 .fetch_add(1, Ordering::Relaxed);
274 let mut secondary = Box::pin(self.execute(hedge, hash, expected_bytes));
275 let first_failure = tokio::select! {
276 result = &mut primary => match result {
277 Ok(()) => return Ok(AttemptResult::Success),
278 Err(error) => (provider, error),
279 },
280 result = &mut secondary => match result {
281 Ok(()) => return Ok(AttemptResult::Success),
282 Err(error) => (hedge_provider, error),
283 },
284 };
285 let second_failure = if first_failure.0 == provider {
286 secondary.await.err().map(|error| (hedge_provider, error))
287 } else {
288 primary.await.err().map(|error| (provider, error))
289 };
290 Ok(second_failure.map_or(AttemptResult::Success, |failure| {
291 AttemptResult::Failed(vec![first_failure, failure])
292 }))
293 }
294
295 async fn reserve(
296 &self,
297 excluded: &BTreeSet<usize>,
298 expected_bytes: u64,
299 policy: &Arc<SharedFetch>,
300 ) -> Result<RemoteAttempt, BlobError> {
301 loop {
302 let notified = self.available.notified();
303 let permit = self
304 .scheduler
305 .acquire_promotable(policy.priority.clone())
306 .await?;
307 let (provider, delay) = {
308 let mut book = self
309 .metrics
310 .providers
311 .lock()
312 .map_err(|_| BlobError::SchedulerUnavailable)?;
313 let now = Instant::now();
314 (
315 book.select(excluded, expected_bytes, now),
316 book.next_ready(excluded, now),
317 )
318 };
319 if let Some(provider) = provider {
320 return Ok(RemoteAttempt {
321 provider,
322 permit: Some(permit),
323 providers: self.metrics.providers.clone(),
324 available: self.available.clone(),
325 finished: false,
326 });
327 }
328 drop(permit);
329 if excluded.len() == self.providers.len() {
330 return Err(BlobError::NoProviders);
331 }
332 if let Some(delay) = delay {
333 tokio::select! {
334 () = notified => {}
335 () = tokio::time::sleep(delay) => {}
336 }
337 } else {
338 notified.await;
339 }
340 }
341 }
342
343 async fn execute(
344 &self,
345 mut attempt: RemoteAttempt,
346 hash: BlobHash,
347 expected_bytes: u64,
348 ) -> Result<(), BlobError> {
349 self.metrics
350 .counters
351 .provider_attempts
352 .fetch_add(1, Ordering::Relaxed);
353 let provider = attempt.provider;
354 match self
355 .local
356 .fetch_with_stats(self.providers[provider].clone(), hash)
357 .await
358 {
359 Ok(stats) => {
360 let bytes = stats.bytes.max(expected_bytes).max(1);
361 attempt.success(bytes, stats.elapsed)?;
362 self.metrics
363 .counters
364 .bytes_fetched
365 .fetch_add(bytes, Ordering::Relaxed);
366 Ok(())
367 }
368 Err(error) => {
369 attempt.failure()?;
370 Err(BlobError::from(error))
371 }
372 }
373 }
374
375 fn hedge_delay(&self, provider: usize) -> Result<Duration, BlobError> {
376 self.metrics
377 .providers
378 .lock()
379 .map_err(|_| BlobError::SchedulerUnavailable)
380 .map(|book| book.hedge_delay(provider))
381 }
382
383 fn record_success(&self) {
384 self.metrics
385 .counters
386 .chunks_fetched
387 .fetch_add(1, Ordering::Relaxed);
388 }
389}
390
391enum AttemptResult {
392 Success,
393 Failed(Vec<(usize, BlobError)>),
394}
395
396fn single_attempt_result(provider: usize, result: Result<(), BlobError>) -> AttemptResult {
397 match result {
398 Ok(()) => AttemptResult::Success,
399 Err(error) => AttemptResult::Failed(vec![(provider, error)]),
400 }
401}
402
403struct SharedFetch {
404 priority: PrioritySignal,
405 allow_hedge: AtomicBool,
406 promoted: Notify,
407 result: Mutex<Option<Result<(), BlobError>>>,
408 completed: Notify,
409}
410
411impl SharedFetch {
412 fn new(priority: BlobPriority, allow_hedge: bool) -> Self {
413 Self {
414 priority: PrioritySignal::new(priority),
415 allow_hedge: AtomicBool::new(allow_hedge),
416 promoted: Notify::new(),
417 result: Mutex::new(None),
418 completed: Notify::new(),
419 }
420 }
421
422 fn promote(&self, priority: BlobPriority, allow_hedge: bool) {
423 let previous = self.priority.get();
424 self.priority.promote(priority);
425 let hedge_was_enabled = self.allow_hedge.fetch_or(allow_hedge, Ordering::AcqRel);
426 if self.priority.get() != previous || (allow_hedge && !hedge_was_enabled) {
427 self.promoted.notify_waiters();
428 }
429 }
430
431 fn should_hedge(&self) -> bool {
432 self.priority.get() == BlobPriority::High && self.allow_hedge.load(Ordering::Acquire)
433 }
434
435 async fn wait(&self) -> Result<(), BlobError> {
436 loop {
437 let notified = self.completed.notified();
438 if let Some(result) = self
439 .result
440 .lock()
441 .map_err(|_| BlobError::SchedulerUnavailable)?
442 .clone()
443 {
444 return result;
445 }
446 notified.await;
447 }
448 }
449
450 fn complete(&self, result: Result<(), BlobError>) {
451 if let Ok(mut slot) = self.result.lock() {
452 *slot = Some(result);
453 }
454 self.completed.notify_waiters();
455 }
456}
457
458struct SharedFetchOwner {
459 pool: ProviderPool,
460 hash: BlobHash,
461 shared: Arc<SharedFetch>,
462 completed: bool,
463}
464
465impl SharedFetchOwner {
466 fn new(pool: ProviderPool, hash: BlobHash, shared: Arc<SharedFetch>) -> Self {
467 Self {
468 pool,
469 hash,
470 shared,
471 completed: false,
472 }
473 }
474
475 fn complete(mut self, result: Result<(), BlobError>) -> Result<(), BlobError> {
476 self.shared.complete(result.clone());
477 self.remove();
478 self.completed = true;
479 result
480 }
481
482 fn remove(&self) {
483 if let Ok(mut in_flight) = self.pool.in_flight.lock() {
484 in_flight.remove(&self.hash);
485 }
486 }
487}
488
489impl Drop for SharedFetchOwner {
490 fn drop(&mut self) {
491 if !self.completed {
492 self.shared.complete(Err(BlobError::FetchCancelled));
493 self.remove();
494 }
495 }
496}
497
498struct RemoteAttempt {
499 provider: usize,
500 permit: Option<TransferPermit>,
501 providers: Arc<Mutex<ProviderBook>>,
502 available: Arc<Notify>,
503 finished: bool,
504}
505
506impl RemoteAttempt {
507 fn success(&mut self, bytes: u64, elapsed: Duration) -> Result<(), BlobError> {
508 self.providers
509 .lock()
510 .map_err(|_| BlobError::SchedulerUnavailable)?
511 .finish_success(self.provider, bytes, elapsed);
512 self.finish();
513 Ok(())
514 }
515
516 fn failure(&mut self) -> Result<(), BlobError> {
517 self.providers
518 .lock()
519 .map_err(|_| BlobError::SchedulerUnavailable)?
520 .finish_failure(self.provider, Instant::now());
521 self.finish();
522 Ok(())
523 }
524
525 fn finish(&mut self) {
526 self.finished = true;
527 self.permit.take();
528 self.available.notify_waiters();
529 }
530}
531
532impl Drop for RemoteAttempt {
533 fn drop(&mut self) {
534 if !self.finished {
535 if let Ok(mut providers) = self.providers.lock() {
536 providers.cancel(self.provider);
537 }
538 self.permit.take();
539 self.available.notify_waiters();
540 }
541 }
542}
543
544#[derive(Clone)]
546pub struct BlobEngine {
547 pub(crate) store: Arc<dyn BlobStore>,
548 pub(crate) domain_id: DomainId,
549 pub(crate) epoch: u64,
550 pub(crate) chunk_size: usize,
551 pub(crate) key: DomainKey,
552 pub(crate) scheduler: FetchScheduler,
553}
554
555impl BlobEngine {
556 pub fn new(store: Arc<dyn BlobStore>, domain_id: DomainId, key: DomainKey) -> Self {
558 Self {
559 store,
560 domain_id,
561 epoch: 0,
562 chunk_size: DEFAULT_CHUNK_SIZE,
563 key,
564 scheduler: FetchScheduler::default(),
565 }
566 }
567
568 pub fn new_at_epoch(
570 store: Arc<dyn BlobStore>,
571 domain_id: DomainId,
572 epoch: u64,
573 key: DomainKey,
574 ) -> Self {
575 Self {
576 store,
577 domain_id,
578 epoch,
579 chunk_size: DEFAULT_CHUNK_SIZE,
580 key,
581 scheduler: FetchScheduler::default(),
582 }
583 }
584
585 pub fn with_chunk_size(
587 store: Arc<dyn BlobStore>,
588 domain_id: DomainId,
589 chunk_size: usize,
590 key: DomainKey,
591 ) -> Result<Self, BlobError> {
592 if !(MIN_CHUNK_SIZE..=MAX_CHUNK_SIZE).contains(&chunk_size) {
593 return Err(BlobError::InvalidChunkSize(chunk_size));
594 }
595 Ok(Self {
596 store,
597 domain_id,
598 epoch: 0,
599 chunk_size,
600 key,
601 scheduler: FetchScheduler::default(),
602 })
603 }
604
605 #[must_use]
607 pub fn with_fetch_scheduler(mut self, scheduler: FetchScheduler) -> Self {
608 self.scheduler = scheduler;
609 self
610 }
611
612 pub async fn import_bytes(
614 &self,
615 bytes: Vec<u8>,
616 media_type: Option<String>,
617 ) -> Result<BlobRef, BlobError> {
618 self.import_reader(bytes.as_slice(), media_type).await
619 }
620
621 pub async fn import_reader(
623 &self,
624 mut reader: impl AsyncRead + Unpin,
625 media_type: Option<String>,
626 ) -> Result<BlobRef, BlobError> {
627 let mut salt = [0_u8; 32];
628 getrandom::fill(&mut salt).map_err(|_| BlobError::RandomnessUnavailable)?;
629 let mut chunks = Vec::new();
630 let mut plaintext_len = 0_u64;
631 let mut plaintext_hasher = blake3::Hasher::new();
632 loop {
633 let mut plaintext = vec![0_u8; self.chunk_size];
634 let mut filled = 0;
635 while filled < plaintext.len() {
636 let count = reader
637 .read(&mut plaintext[filled..])
638 .await
639 .map_err(|error| BlobError::Read(error.to_string()))?;
640 if count == 0 {
641 break;
642 }
643 filled += count;
644 }
645 if filled == 0 {
646 break;
647 }
648 plaintext.truncate(filled);
649 let index = chunks.len();
650 let index = u32::try_from(index).map_err(|_| BlobError::BlobTooLarge)?;
651 let chunk_plaintext_len =
652 u32::try_from(plaintext.len()).map_err(|_| BlobError::BlobTooLarge)?;
653 let mut nonce = [0_u8; 24];
654 getrandom::fill(&mut nonce).map_err(|_| BlobError::RandomnessUnavailable)?;
655 let ciphertext = encrypt_chunk(
656 &self.key,
657 self.domain_id,
658 self.epoch,
659 &salt,
660 index,
661 chunk_plaintext_len,
662 nonce,
663 &plaintext,
664 )?;
665 let ciphertext_hash = self.store.put(ciphertext).await.map_err(BlobError::from)?;
666 chunks.push(ChunkDescriptor {
667 index,
668 plaintext_len: chunk_plaintext_len,
669 ciphertext_hash,
670 nonce,
671 });
672 plaintext_hasher.update(&plaintext);
673 plaintext_len = plaintext_len
674 .checked_add(u64::try_from(filled).map_err(|_| BlobError::BlobTooLarge)?)
675 .ok_or(BlobError::BlobTooLarge)?;
676 if filled < self.chunk_size {
677 break;
678 }
679 }
680 let chunk_size = u32::try_from(self.chunk_size).map_err(|_| BlobError::BlobTooLarge)?;
681 let body = ManifestBody {
682 plaintext_len,
683 plaintext_hash: *plaintext_hasher.finalize().as_bytes(),
684 chunk_size,
685 chunks,
686 media_type,
687 };
688 let plaintext = minicbor::to_vec(&body).map_err(codec_error)?;
689 let mut nonce = [0_u8; 24];
690 getrandom::fill(&mut nonce).map_err(|_| BlobError::RandomnessUnavailable)?;
691 let ciphertext = encrypt_manifest(
692 &self.key,
693 self.domain_id,
694 self.epoch,
695 &salt,
696 nonce,
697 &plaintext,
698 )?;
699 let envelope = ManifestEnvelope {
700 version: FORMAT_VERSION,
701 domain_id: self.domain_id,
702 epoch: self.epoch,
703 salt,
704 nonce,
705 ciphertext,
706 };
707 let encoded = minicbor::to_vec(envelope).map_err(codec_error)?;
708 let manifest_hash = self.store.put(encoded).await.map_err(BlobError::from)?;
709 Ok(BlobRef::new(self.domain_id, self.epoch, manifest_hash))
710 }
711
712 pub async fn open(&self, reference: &BlobRef) -> Result<BlobStream, BlobError> {
714 if reference.domain_id() != self.domain_id || reference.epoch() != self.epoch {
715 return Err(BlobError::WrongDomainOrEpoch);
716 }
717 let encoded = self
718 .store
719 .get(reference.manifest_hash())
720 .await
721 .map_err(BlobError::from)?
722 .ok_or(BlobError::MissingBlob(reference.manifest_hash()))?;
723 verify_hash(reference.manifest_hash(), &encoded)?;
724 let envelope: ManifestEnvelope = decode_canonical(&encoded)?;
725 if envelope.version != FORMAT_VERSION
726 || envelope.domain_id != self.domain_id
727 || envelope.epoch != self.epoch
728 {
729 return Err(BlobError::WrongDomainOrEpoch);
730 }
731 let plaintext = decrypt_manifest(&self.key, &envelope)?;
732 let body: ManifestBody = decode_canonical(&plaintext)?;
733 validate_manifest(&body)?;
734 Ok(BlobStream::new(
735 self.store.clone(),
736 self.key.clone(),
737 envelope,
738 body,
739 ))
740 }
741
742 pub async fn open_from_providers(
744 &self,
745 local: IrohBlobStore,
746 mut providers: Vec<iroh::endpoint::Connection>,
747 reference: &BlobRef,
748 options: FetchOptions,
749 ) -> Result<(BlobStream, FetchMetrics), BlobError> {
750 if reference.domain_id() != self.domain_id || reference.epoch() != self.epoch {
751 return Err(BlobError::WrongDomainOrEpoch);
752 }
753 let mut endpoint_ids = BTreeSet::new();
754 providers.retain(|connection| endpoint_ids.insert(connection.remote_id()));
755 let metrics = FetchMetrics::new(providers.len());
756 let pool = ProviderPool {
757 local,
758 claims: self.store.clone(),
759 providers: Arc::new(providers),
760 scheduler: self.scheduler.clone(),
761 available: Arc::new(Notify::new()),
762 in_flight: Arc::new(Mutex::new(BTreeMap::new())),
763 metrics: metrics.clone(),
764 };
765 if self
766 .store
767 .get(reference.manifest_hash())
768 .await
769 .map_err(BlobError::from)?
770 .is_none()
771 {
772 pool.fetch_unclaimed(
773 reference.manifest_hash(),
774 0,
775 options.priority,
776 options.priority == BlobPriority::High,
777 )
778 .await?;
779 }
780 let encoded = self
781 .store
782 .get(reference.manifest_hash())
783 .await
784 .map_err(BlobError::from)?
785 .ok_or(BlobError::MissingBlob(reference.manifest_hash()))?;
786 verify_hash(reference.manifest_hash(), &encoded)?;
787 let envelope: ManifestEnvelope = decode_canonical(&encoded)?;
788 if envelope.version != FORMAT_VERSION
789 || envelope.domain_id != self.domain_id
790 || envelope.epoch != self.epoch
791 {
792 return Err(BlobError::WrongDomainOrEpoch);
793 }
794 let plaintext = decrypt_manifest(&self.key, &envelope)?;
795 let body: ManifestBody = decode_canonical(&plaintext)?;
796 validate_manifest(&body)?;
797 self.store
798 .claim(reference.manifest_hash())
799 .await
800 .map_err(BlobError::from)?;
801
802 let fetch_pool = pool.clone();
803 let fetcher = Arc::new(move |hash, expected_bytes, priority, allow_hedge| {
804 let pool = fetch_pool.clone();
805 let future: Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'static>> =
806 Box::pin(async move {
807 pool.fetch(hash, expected_bytes, priority, allow_hedge)
808 .await
809 });
810 future
811 });
812 Ok((
813 BlobStream::new(self.store.clone(), self.key.clone(), envelope, body).with_fetcher(
814 fetcher,
815 options.priority,
816 options.prefetch_chunks,
817 ),
818 metrics,
819 ))
820 }
821
822 pub async fn fetch_from(
824 &self,
825 local: &IrohBlobStore,
826 connection: iroh::endpoint::Connection,
827 reference: &BlobRef,
828 ) -> Result<(), BlobError> {
829 if reference.domain_id() != self.domain_id || reference.epoch() != self.epoch {
830 return Err(BlobError::WrongDomainOrEpoch);
831 }
832 local
833 .fetch(connection.clone(), reference.manifest_hash())
834 .await
835 .map_err(BlobError::from)?;
836 let encoded = self
837 .store
838 .get(reference.manifest_hash())
839 .await
840 .map_err(BlobError::from)?
841 .ok_or(BlobError::MissingBlob(reference.manifest_hash()))?;
842 verify_hash(reference.manifest_hash(), &encoded)?;
843 let envelope: ManifestEnvelope = decode_canonical(&encoded)?;
844 if envelope.version != FORMAT_VERSION
845 || envelope.domain_id != self.domain_id
846 || envelope.epoch != self.epoch
847 {
848 return Err(BlobError::WrongDomainOrEpoch);
849 }
850 let plaintext = decrypt_manifest(&self.key, &envelope)?;
851 let body: ManifestBody = decode_canonical(&plaintext)?;
852 validate_manifest(&body)?;
853 self.store
854 .claim(reference.manifest_hash())
855 .await
856 .map_err(BlobError::from)?;
857 for chunk in body.chunks {
858 local
859 .fetch(connection.clone(), chunk.ciphertext_hash)
860 .await
861 .map_err(BlobError::from)?;
862 self.store
863 .claim(chunk.ciphertext_hash)
864 .await
865 .map_err(BlobError::from)?;
866 }
867 Ok(())
868 }
869}
870
871pub(crate) fn decrypt_chunk(
872 key: &DomainKey,
873 domain_id: DomainId,
874 epoch: u64,
875 salt: &[u8; 32],
876 descriptor: &ChunkDescriptor,
877 ciphertext: &[u8],
878) -> Result<Vec<u8>, BlobError> {
879 verify_hash(descriptor.ciphertext_hash, ciphertext)?;
880 let key = chunk_key(key, domain_id, epoch, salt, descriptor.index)?;
881 let cipher = XChaCha20Poly1305::new((&*key).into());
882 let aad = chunk_aad(
883 domain_id,
884 epoch,
885 salt,
886 descriptor.index,
887 descriptor.plaintext_len,
888 );
889 let plaintext = cipher
890 .decrypt(
891 &XNonce::from(descriptor.nonce),
892 Payload {
893 msg: ciphertext,
894 aad: &aad,
895 },
896 )
897 .map_err(|_| BlobError::AuthenticationFailed)?;
898 if plaintext.len() != descriptor.plaintext_len as usize {
899 return Err(BlobError::InvalidManifest);
900 }
901 Ok(plaintext)
902}
903
904#[allow(clippy::too_many_arguments)]
906fn encrypt_chunk(
907 key: &DomainKey,
908 domain_id: DomainId,
909 epoch: u64,
910 salt: &[u8; 32],
911 index: u32,
912 plaintext_len: u32,
913 nonce: [u8; 24],
914 plaintext: &[u8],
915) -> Result<Vec<u8>, BlobError> {
916 let key = chunk_key(key, domain_id, epoch, salt, index)?;
917 let cipher = XChaCha20Poly1305::new((&*key).into());
918 let aad = chunk_aad(domain_id, epoch, salt, index, plaintext_len);
919 cipher
920 .encrypt(
921 &XNonce::from(nonce),
922 Payload {
923 msg: plaintext,
924 aad: &aad,
925 },
926 )
927 .map_err(|_| BlobError::EncryptionFailed)
928}
929
930fn chunk_key(
931 key: &DomainKey,
932 domain_id: DomainId,
933 epoch: u64,
934 salt: &[u8; 32],
935 index: u32,
936) -> Result<zeroize::Zeroizing<[u8; 32]>, BlobError> {
937 let mut derivation_salt = Vec::with_capacity(64);
938 derivation_salt.extend_from_slice(domain_id.as_bytes());
939 derivation_salt.extend_from_slice(salt);
940 let mut context = Vec::with_capacity(37);
941 context.extend_from_slice(b"iroh-db/blob-chunk/v1");
942 context.extend_from_slice(&epoch.to_be_bytes());
943 context.extend_from_slice(&index.to_be_bytes());
944 key.derive_subkey(&derivation_salt, &context)
945 .map_err(|_| BlobError::KeyDerivationFailed)
946}
947
948fn chunk_aad(
949 domain_id: DomainId,
950 epoch: u64,
951 salt: &[u8; 32],
952 index: u32,
953 plaintext_len: u32,
954) -> Vec<u8> {
955 let mut aad = Vec::with_capacity(80);
956 aad.extend_from_slice(b"iroh-db/blob-chunk-aad/v1");
957 aad.extend_from_slice(domain_id.as_bytes());
958 aad.extend_from_slice(&epoch.to_be_bytes());
959 aad.extend_from_slice(salt);
960 aad.extend_from_slice(&index.to_be_bytes());
961 aad.extend_from_slice(&plaintext_len.to_be_bytes());
962 aad
963}
964
965fn encrypt_manifest(
966 key: &DomainKey,
967 domain_id: DomainId,
968 epoch: u64,
969 salt: &[u8; 32],
970 nonce: [u8; 24],
971 plaintext: &[u8],
972) -> Result<Vec<u8>, BlobError> {
973 let key = manifest_key(key, domain_id, epoch, salt)?;
974 let cipher = XChaCha20Poly1305::new((&*key).into());
975 let aad = manifest_aad(domain_id, epoch, salt, &nonce);
976 cipher
977 .encrypt(
978 &XNonce::from(nonce),
979 Payload {
980 msg: plaintext,
981 aad: &aad,
982 },
983 )
984 .map_err(|_| BlobError::EncryptionFailed)
985}
986
987fn decrypt_manifest(key: &DomainKey, envelope: &ManifestEnvelope) -> Result<Vec<u8>, BlobError> {
988 let key = manifest_key(key, envelope.domain_id, envelope.epoch, &envelope.salt)?;
989 let cipher = XChaCha20Poly1305::new((&*key).into());
990 let aad = manifest_aad(
991 envelope.domain_id,
992 envelope.epoch,
993 &envelope.salt,
994 &envelope.nonce,
995 );
996 cipher
997 .decrypt(
998 &XNonce::from(envelope.nonce),
999 Payload {
1000 msg: &envelope.ciphertext,
1001 aad: &aad,
1002 },
1003 )
1004 .map_err(|_| BlobError::AuthenticationFailed)
1005}
1006
1007fn manifest_key(
1008 key: &DomainKey,
1009 domain_id: DomainId,
1010 epoch: u64,
1011 salt: &[u8; 32],
1012) -> Result<zeroize::Zeroizing<[u8; 32]>, BlobError> {
1013 let mut derivation_salt = Vec::with_capacity(64);
1014 derivation_salt.extend_from_slice(domain_id.as_bytes());
1015 derivation_salt.extend_from_slice(salt);
1016 let mut context = Vec::with_capacity(32);
1017 context.extend_from_slice(b"iroh-db/blob-manifest/v1");
1018 context.extend_from_slice(&epoch.to_be_bytes());
1019 key.derive_subkey(&derivation_salt, &context)
1020 .map_err(|_| BlobError::KeyDerivationFailed)
1021}
1022
1023fn manifest_aad(domain_id: DomainId, epoch: u64, salt: &[u8; 32], nonce: &[u8; 24]) -> Vec<u8> {
1024 let mut aad = Vec::with_capacity(104);
1025 aad.extend_from_slice(b"iroh-db/blob-manifest-aad/v1");
1026 aad.extend_from_slice(&FORMAT_VERSION.to_be_bytes());
1027 aad.extend_from_slice(domain_id.as_bytes());
1028 aad.extend_from_slice(&epoch.to_be_bytes());
1029 aad.extend_from_slice(salt);
1030 aad.extend_from_slice(nonce);
1031 aad
1032}
1033
1034pub(crate) fn verify_hash(expected: BlobHash, bytes: &[u8]) -> Result<(), BlobError> {
1035 let actual = BlobHash::from_bytes(*blake3::hash(bytes).as_bytes());
1036 if actual != expected {
1037 return Err(BlobError::HashMismatch);
1038 }
1039 Ok(())
1040}
1041
1042fn decode_canonical<T>(bytes: &[u8]) -> Result<T, BlobError>
1043where
1044 T: minicbor::Encode<()> + for<'value> minicbor::Decode<'value, ()>,
1045{
1046 let mut decoder = minicbor::Decoder::new(bytes);
1047 let value = decoder.decode().map_err(codec_error)?;
1048 if decoder.position() != bytes.len() {
1049 return Err(BlobError::InvalidManifest);
1050 }
1051 let canonical = minicbor::to_vec(&value).map_err(codec_error)?;
1052 if canonical != bytes {
1053 return Err(BlobError::InvalidManifest);
1054 }
1055 Ok(value)
1056}
1057
1058#[allow(clippy::needless_pass_by_value)]
1059fn codec_error(error: impl std::fmt::Display) -> BlobError {
1060 BlobError::Codec(error.to_string())
1061}
1062
1063impl From<BlobStoreError> for BlobError {
1064 fn from(error: BlobStoreError) -> Self {
1065 Self::Store(error.to_string())
1066 }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use iroh::{Endpoint, RelayMode, endpoint::presets, protocol::Router};
1072 use iroh_blobs::provider::events::{EventMask, EventSender, ProviderMessage, RequestMode};
1073
1074 use super::*;
1075
1076 #[tokio::test]
1077 async fn stalled_high_priority_fetch_hedges_to_another_provider() {
1078 let bytes = vec![42_u8; 1024 * 1024];
1079 let (slow_dir, slow_store, slow_router, hash) =
1080 start_provider(&bytes, Some(Duration::from_millis(500))).await;
1081 let (fast_dir, fast_store, fast_router, fast_hash) = start_provider(&bytes, None).await;
1082 assert_eq!(hash, fast_hash);
1083 let client_dir = tempfile::tempdir().unwrap();
1084 let client_store = IrohBlobStore::load(client_dir.path()).await.unwrap();
1085 let endpoint = Endpoint::builder(presets::Minimal)
1086 .relay_mode(RelayMode::Disabled)
1087 .bind()
1088 .await
1089 .unwrap();
1090 let slow = endpoint
1091 .connect(slow_router.endpoint().addr(), iroh_blobs::ALPN)
1092 .await
1093 .unwrap();
1094 let fast = endpoint
1095 .connect(fast_router.endpoint().addr(), iroh_blobs::ALPN)
1096 .await
1097 .unwrap();
1098 let metrics = FetchMetrics::new(2);
1099 let pool = ProviderPool {
1100 local: client_store.clone(),
1101 claims: Arc::new(client_store.clone()),
1102 providers: Arc::new(vec![slow, fast]),
1103 scheduler: FetchScheduler::default(),
1104 available: Arc::new(Notify::new()),
1105 in_flight: Arc::new(Mutex::new(BTreeMap::new())),
1106 metrics: metrics.clone(),
1107 };
1108
1109 tokio::time::timeout(
1110 Duration::from_secs(2),
1111 pool.fetch_unclaimed(hash, bytes.len() as u64, BlobPriority::High, true),
1112 )
1113 .await
1114 .unwrap()
1115 .unwrap();
1116 assert_eq!(metrics.hedged_attempts(), 1);
1117 assert_eq!(metrics.chunks_fetched(), 1);
1118 assert_eq!(metrics.providers()[1].successes(), 1);
1119
1120 endpoint.close().await;
1121 slow_router.shutdown().await.unwrap();
1122 fast_router.shutdown().await.unwrap();
1123 client_store.shutdown().await.unwrap();
1124 slow_store.shutdown().await.unwrap();
1125 fast_store.shutdown().await.unwrap();
1126 drop((slow_dir, fast_dir));
1127 }
1128
1129 async fn start_provider(
1130 bytes: &[u8],
1131 delay: Option<Duration>,
1132 ) -> (tempfile::TempDir, IrohBlobStore, Router, BlobHash) {
1133 let directory = tempfile::tempdir().unwrap();
1134 let store = IrohBlobStore::load(directory.path()).await.unwrap();
1135 let hash = store.put(bytes.to_vec()).await.unwrap();
1136 let events = delay.map_or(EventSender::DEFAULT, |delay| {
1137 let mask = EventMask {
1138 get: RequestMode::Intercept,
1139 ..EventMask::DEFAULT
1140 };
1141 let (events, mut receiver) = EventSender::channel(8, mask);
1142 tokio::spawn(async move {
1143 while let Some(message) = receiver.recv().await {
1144 if let ProviderMessage::GetRequestReceived(message) = message {
1145 tokio::time::sleep(delay).await;
1146 let _ignored = message.tx.send(Ok(())).await;
1147 }
1148 }
1149 });
1150 events
1151 });
1152 let endpoint = Endpoint::builder(presets::Minimal)
1153 .relay_mode(RelayMode::Disabled)
1154 .bind()
1155 .await
1156 .unwrap();
1157 let router = Router::builder(endpoint)
1158 .accept(iroh_blobs::ALPN, store.protocol_with_events(events))
1159 .spawn();
1160 (directory, store, router, hash)
1161 }
1162}