1use std::{
9 collections::{HashMap, HashSet},
10 io,
11 str::FromStr,
12 sync::{Arc, Mutex},
13 time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
17use iroh::{Endpoint, PublicKey, SecretKey};
18use iroh_blobs::{Hash, ticket::BlobTicket};
19use iroh_gossip::{
20 TopicId,
21 api::{Event, GossipSender, GossipTopic},
22};
23use n0_future::StreamExt;
24use serde::{Deserialize, Serialize};
25use tokio::io::{AsyncRead, AsyncReadExt};
26use tokio::{sync::watch, task::JoinHandle};
27
28use crate::{
29 error::{Result, SyncwebError},
30 indexing::{
31 reputation::{ProviderReputationStore, ReputationConfig},
32 wot::{ProviderTrustDecision, WotService},
33 },
34 node::{blob_store::BlobStore, gossip_service::GossipService},
35};
36
37const RESILIENCE_TOPIC_SEED: &[u8] = b"syncweb/provider-leases/v1";
38const PROVIDER_LEASE_SIGNATURE_CONTEXT: &[u8] = b"syncweb/provider-lease/v1\0";
39const DEFAULT_OBSERVATION_TTL: Duration = Duration::from_mins(5);
40const DEFAULT_MAX_JITTER: Duration = Duration::from_secs(30);
41const DEFAULT_RESPONSIBLE_PEERS: usize = 1;
42const DEFAULT_MAX_FAILURES_PER_PROVIDER: usize = 128;
43const DEFAULT_FAILURE_TTL: Duration = Duration::from_hours(24);
44const AUTOMATED_BAN_DURATION: Duration = Duration::from_hours(1);
45const DEFINITIVE_FAILURE_THRESHOLD: u32 = 3;
46const STREAM_VALIDATION_CHUNK_SIZE: usize = 64 * 1024;
47const REPLICATION_PIN_PREFIX: &str = "syncweb/replication/";
48
49#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
51#[non_exhaustive]
52pub enum FetchFailureKind {
53 NotFound,
54 ConnectionRefused,
55 Timeout,
56 Corruption,
57 Unknown,
58}
59
60impl FetchFailureKind {
61 #[must_use]
68 pub fn from_syncweb_error(error: &SyncwebError) -> Self {
69 Self::from_message(error.to_string())
70 }
71
72 #[must_use]
74 pub fn classify(error: &SyncwebError) -> Self {
75 Self::from_syncweb_error(error)
76 }
77
78 #[must_use]
80 pub fn from_error(error: &SyncwebError) -> Self {
81 Self::from_syncweb_error(error)
82 }
83
84 #[must_use]
86 pub fn from_message(message: impl AsRef<str>) -> Self {
87 let message_text = message.as_ref().to_ascii_lowercase();
88 if [
89 "not found",
90 "does not exist",
91 "no such file",
92 "missing blob",
93 "unknown hash",
94 "404",
95 ]
96 .iter()
97 .any(|phrase| message_text.contains(phrase))
98 {
99 return Self::NotFound;
100 }
101 if [
102 "hash mismatch",
103 "checksum mismatch",
104 "integrity",
105 "corrupt",
106 "invalid data",
107 ]
108 .iter()
109 .any(|phrase| message_text.contains(phrase))
110 {
111 return Self::Corruption;
112 }
113 if [
114 "connection refused",
115 "connection reset",
116 "network is unreachable",
117 "no route to host",
118 "unreachable",
119 ]
120 .iter()
121 .any(|phrase| message_text.contains(phrase))
122 {
123 return Self::ConnectionRefused;
124 }
125 if ["timed out", "timeout", "deadline exceeded"]
126 .iter()
127 .any(|phrase| message_text.contains(phrase))
128 {
129 return Self::Timeout;
130 }
131 Self::Unknown
132 }
133
134 #[must_use]
135 pub const fn is_definitive(self) -> bool {
136 matches!(self, Self::NotFound | Self::Corruption)
137 }
138
139 #[must_use]
140 pub const fn is_transient(self) -> bool {
141 !self.is_definitive()
142 }
143}
144
145impl From<&SyncwebError> for FetchFailureKind {
146 fn from(error: &SyncwebError) -> Self {
147 Self::from_syncweb_error(error)
148 }
149}
150
151#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
153#[non_exhaustive]
154pub struct FetchFailure {
155 pub kind: FetchFailureKind,
156 pub provider: PublicKey,
157 pub hash: Hash,
158 pub timestamp: u64,
159 pub error_detail: String,
160}
161
162impl FetchFailure {
163 #[must_use]
165 pub fn new(kind: FetchFailureKind, provider: PublicKey, hash: Hash, error_detail: impl Into<String>) -> Self {
166 Self::new_at(kind, provider, hash, current_epoch_seconds(), error_detail)
167 }
168
169 #[must_use]
171 pub fn new_at(
172 kind: FetchFailureKind,
173 provider: PublicKey,
174 hash: Hash,
175 timestamp: u64,
176 error_detail: impl Into<String>,
177 ) -> Self {
178 Self {
179 kind,
180 provider,
181 hash,
182 timestamp,
183 error_detail: error_detail.into(),
184 }
185 }
186
187 #[must_use]
189 pub fn from_syncweb_error(provider: PublicKey, hash: Hash, error: &SyncwebError) -> Self {
190 Self::new(
191 FetchFailureKind::from_syncweb_error(error),
192 provider,
193 hash,
194 error.to_string(),
195 )
196 }
197
198 #[must_use]
200 pub fn from_error(provider: PublicKey, hash: Hash, error: &SyncwebError) -> Self {
201 Self::from_syncweb_error(provider, hash, error)
202 }
203
204 pub fn to_bytes(&self) -> Result<Vec<u8>> {
210 serde_json::to_vec(self).map_err(|error| SyncwebError::operation("failed to serialize fetch failure", error))
211 }
212
213 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
219 serde_json::from_slice(bytes.as_ref())
220 .map_err(|error| SyncwebError::operation("failed to deserialize fetch failure", error))
221 }
222}
223
224pub async fn validate_bounded_fetch<R>(mut reader: R, expected_size: u64, expected_hash: Hash) -> Result<()>
235where
236 R: AsyncRead + Unpin,
237{
238 let mut hasher = blake3::Hasher::new();
239 let mut remaining = expected_size;
240 let mut buffer = vec![0_u8; STREAM_VALIDATION_CHUNK_SIZE];
241 while remaining > 0 {
242 let requested = usize::try_from(remaining).map_or(STREAM_VALIDATION_CHUNK_SIZE, |remaining_size| {
243 remaining_size.min(STREAM_VALIDATION_CHUNK_SIZE)
244 });
245 let read_buffer = buffer
246 .get_mut(..requested)
247 .ok_or_else(|| SyncwebError::InvalidConfig("invalid bounded fetch buffer size".to_owned()))?;
248 let read = reader.read(read_buffer).await?;
249 if read == 0 {
250 return Err(SyncwebError::operation(
251 "fetched blob validation failed",
252 io::Error::new(io::ErrorKind::UnexpectedEof, "provider stream is truncated"),
253 ));
254 }
255 let data = buffer
256 .get(..read)
257 .ok_or_else(|| SyncwebError::InvalidConfig("invalid bounded fetch read size".to_owned()))?;
258 hasher.update(data);
259 remaining = remaining.saturating_sub(u64::try_from(read).unwrap_or(u64::MAX));
260 }
261
262 let read_buffer = buffer
263 .get_mut(..1)
264 .ok_or_else(|| SyncwebError::InvalidConfig("invalid bounded fetch buffer size".to_owned()))?;
265 let read = reader.read(read_buffer).await?;
266 if read != 0 {
267 return Err(SyncwebError::operation(
268 "fetched blob validation failed",
269 io::Error::new(io::ErrorKind::InvalidData, "provider stream exceeds expected size"),
270 ));
271 }
272
273 let actual_hash = Hash::from_bytes(*hasher.finalize().as_bytes());
274 if actual_hash != expected_hash {
275 return Err(SyncwebError::operation(
276 "fetched blob validation failed",
277 io::Error::new(io::ErrorKind::InvalidData, "provider stream hash mismatch"),
278 ));
279 }
280 Ok(())
281}
282
283pub async fn validate_fetch_stream<R>(reader: R, expected_size: u64, expected_hash: Hash) -> Result<()>
289where
290 R: AsyncRead + Unpin,
291{
292 validate_bounded_fetch(reader, expected_size, expected_hash).await
293}
294
295pub async fn validate_bounded_stream<R>(reader: R, expected_size: u64, expected_hash: Hash) -> Result<()>
301where
302 R: AsyncRead + Unpin,
303{
304 validate_bounded_fetch(reader, expected_size, expected_hash).await
305}
306
307#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
312#[non_exhaustive]
313pub struct ProviderLease {
314 pub hash: Hash,
315 pub provider: PublicKey,
316 pub ticket: String,
317 pub sequence: u64,
318 pub issued_at: u64,
319 pub expires_at: u64,
320 pub signature: Option<String>,
321}
322
323impl ProviderLease {
324 pub fn new(hash: Hash, ticket: impl Into<String>, sequence: u64, expires_at: u64) -> Result<Self> {
332 Self::new_with_times(hash, ticket, sequence, current_epoch_seconds(), expires_at)
333 }
334
335 pub fn new_with_times(
345 hash: Hash,
346 ticket: impl Into<String>,
347 sequence: u64,
348 issued_at: u64,
349 expires_at: u64,
350 ) -> Result<Self> {
351 let ticket_text = ticket.into();
352 let parsed_ticket = parse_ticket(&ticket_text)?;
353 if parsed_ticket.hash() != hash {
354 return Err(SyncwebError::InvalidTicket(
355 "provider lease ticket does not match its blob hash".to_owned(),
356 ));
357 }
358 let lease = Self {
359 hash,
360 provider: parsed_ticket.addr().id,
361 ticket: ticket_text,
362 sequence,
363 issued_at,
364 expires_at,
365 signature: None,
366 };
367 lease.validate()?;
368 Ok(lease)
369 }
370
371 pub fn signed(
378 hash: Hash,
379 ticket: impl Into<String>,
380 sequence: u64,
381 expires_at: u64,
382 secret_key: &SecretKey,
383 ) -> Result<Self> {
384 let mut lease = Self::new(hash, ticket, sequence, expires_at)?;
385 lease.sign_with_secret_key(secret_key)?;
386 Ok(lease)
387 }
388
389 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
398 if signing_key.verifying_key().to_bytes() != *self.provider.as_bytes() {
399 return Err(SyncwebError::InvalidIdentity(
400 "provider lease signer does not match the ticket provider".to_owned(),
401 ));
402 }
403 self.signature = Some(hex::encode(signing_key.sign(&self.unsigned_bytes()?).to_bytes()));
404 Ok(())
405 }
406
407 pub fn sign_with_secret_key(&mut self, secret_key: &SecretKey) -> Result<()> {
414 let signing_key = SigningKey::from_bytes(&secret_key.to_bytes());
415 self.sign(&signing_key)
416 }
417
418 #[must_use]
420 pub const fn is_signed(&self) -> bool {
421 self.signature.is_some()
422 }
423
424 pub fn verify_signature(&self) -> Result<()> {
430 self.validate()?;
431 let signature_text = self
432 .signature
433 .as_deref()
434 .ok_or_else(|| SyncwebError::InvalidConfig("provider lease must contain a signature".to_owned()))?;
435 let signature_bytes = hex::decode(signature_text).map_err(|error| {
436 SyncwebError::InvalidConfig(format!("invalid provider lease signature encoding: {error}"))
437 })?;
438 let signature = Signature::from_slice(&signature_bytes)
439 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid provider lease signature: {error}")))?;
440 let verifying_key = VerifyingKey::from_bytes(self.provider.as_bytes())
441 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider lease identity: {error}")))?;
442 verifying_key
443 .verify(&self.unsigned_bytes()?, &signature)
444 .map_err(|error| SyncwebError::InvalidConfig(format!("provider lease signature is invalid: {error}")))
445 }
446
447 pub fn verify_at(&self, now: u64) -> Result<()> {
453 self.verify_signature()?;
454 if self.expires_at <= now {
455 return Err(SyncwebError::InvalidConfig("provider lease has expired".to_owned()));
456 }
457 Ok(())
458 }
459
460 #[must_use]
462 pub const fn is_expired_at(&self, now: u64) -> bool {
463 self.expires_at <= now
464 }
465
466 pub fn to_bytes(&self) -> Result<Vec<u8>> {
472 self.validate()?;
473 serde_json::to_vec(self).map_err(|error| SyncwebError::operation("failed to serialize provider lease", error))
474 }
475
476 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
482 let lease: Self = serde_json::from_slice(bytes.as_ref())
483 .map_err(|error| SyncwebError::operation("failed to deserialize provider lease", error))?;
484 lease.validate()?;
485 Ok(lease)
486 }
487
488 pub fn validate(&self) -> Result<()> {
494 if self.sequence == 0 {
495 return Err(SyncwebError::InvalidConfig(
496 "provider lease sequence must be greater than zero".to_owned(),
497 ));
498 }
499 if self.expires_at <= self.issued_at {
500 return Err(SyncwebError::InvalidConfig(
501 "provider lease expiration must be after its issue time".to_owned(),
502 ));
503 }
504 let ticket = parse_ticket(&self.ticket)?;
505 if ticket.hash() != self.hash {
506 return Err(SyncwebError::InvalidTicket(
507 "provider lease ticket does not match its blob hash".to_owned(),
508 ));
509 }
510 if ticket.addr().id != self.provider {
511 return Err(SyncwebError::InvalidIdentity(
512 "provider lease provider does not match its ticket".to_owned(),
513 ));
514 }
515 Ok(())
516 }
517
518 fn unsigned_bytes(&self) -> Result<Vec<u8>> {
519 let mut unsigned = self.clone();
520 unsigned.signature = None;
521 let encoded = serde_json::to_vec(&unsigned)
522 .map_err(|error| SyncwebError::operation("failed to serialize unsigned provider lease", error))?;
523 let mut signed_bytes = Vec::with_capacity(PROVIDER_LEASE_SIGNATURE_CONTEXT.len().saturating_add(encoded.len()));
524 signed_bytes.extend_from_slice(PROVIDER_LEASE_SIGNATURE_CONTEXT);
525 signed_bytes.extend_from_slice(&encoded);
526 Ok(signed_bytes)
527 }
528}
529
530#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532#[non_exhaustive]
533pub enum LeaseUpdate {
534 Inserted,
535 Replaced,
536 IgnoredOlder,
537}
538
539impl LeaseUpdate {
540 #[must_use]
541 pub const fn changed(self) -> bool {
542 matches!(self, Self::Inserted | Self::Replaced)
543 }
544}
545
546#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
548#[non_exhaustive]
549pub enum BanSource {
550 Manual,
551 Automated,
552 WoT,
553}
554
555#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
557#[non_exhaustive]
558pub struct BanRecord {
559 pub provider: PublicKey,
560 pub hash: Option<Hash>,
561 pub banned_at: u64,
562 pub expires_at: Option<u64>,
563 pub reason: String,
564 pub source: BanSource,
565}
566
567#[derive(Clone, Debug, Eq, PartialEq)]
569#[non_exhaustive]
570pub struct AvailabilityHealth {
571 pub verified: usize,
572 pub local: usize,
573 pub verified_providers: Vec<PublicKey>,
574 pub local_providers: Vec<PublicKey>,
575}
576
577impl AvailabilityHealth {
578 #[must_use]
579 pub const fn verified_count(&self) -> usize {
580 self.verified
581 }
582
583 #[must_use]
584 pub const fn verified_lease_count(&self) -> usize {
585 self.verified
586 }
587
588 #[must_use]
589 pub const fn local_count(&self) -> usize {
590 self.local
591 }
592
593 #[must_use]
594 pub const fn local_observation_count(&self) -> usize {
595 self.local
596 }
597}
598
599#[derive(Clone, Debug)]
601pub struct ProviderLeaseTracker {
602 leases: HashMap<Hash, HashMap<PublicKey, ProviderLease>>,
603 observations: HashMap<Hash, HashMap<PublicKey, u64>>,
604 failures: HashMap<Hash, HashMap<PublicKey, FailureRecord>>,
605 failure_totals: HashMap<Hash, HashMap<PublicKey, u64>>,
606 definitive_streaks: HashMap<Hash, HashMap<PublicKey, u32>>,
607 bans: HashMap<PublicKey, BanRecord>,
608 hash_bans: HashMap<Hash, HashMap<PublicKey, BanRecord>>,
609 max_failures_per_provider: usize,
610}
611
612impl Default for ProviderLeaseTracker {
613 fn default() -> Self {
614 Self::with_max_failures_per_provider(DEFAULT_MAX_FAILURES_PER_PROVIDER)
615 }
616}
617
618#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
620#[non_exhaustive]
621pub struct FailureRecord {
622 pub failures: Vec<FetchFailure>,
623 pub consecutive_failures: u32,
624 pub last_failure_at: u64,
625 pub first_failure_at: u64,
626}
627
628impl ProviderLeaseTracker {
629 #[must_use]
631 pub fn with_max_failures_per_provider(max_failures_per_provider: usize) -> Self {
632 Self {
633 leases: HashMap::new(),
634 observations: HashMap::new(),
635 failures: HashMap::new(),
636 failure_totals: HashMap::new(),
637 definitive_streaks: HashMap::new(),
638 bans: HashMap::new(),
639 hash_bans: HashMap::new(),
640 max_failures_per_provider,
641 }
642 }
643
644 #[must_use]
646 pub const fn max_failures_per_provider(&self) -> usize {
647 self.max_failures_per_provider
648 }
649
650 pub fn set_max_failures_per_provider(&mut self, max_failures_per_provider: usize) {
652 self.max_failures_per_provider = max_failures_per_provider;
653 for providers in self.failures.values_mut() {
654 for record in providers.values_mut() {
655 let keep_from = record.failures.len().saturating_sub(max_failures_per_provider);
656 if keep_from > 0 {
657 record.failures.drain(..keep_from);
658 }
659 }
660 }
661 }
662
663 pub fn track(&mut self, lease: ProviderLease) -> Result<LeaseUpdate> {
669 self.track_at(lease, current_epoch_seconds())
670 }
671
672 pub fn track_at(&mut self, lease: ProviderLease, now: u64) -> Result<LeaseUpdate> {
681 lease.verify_at(now)?;
682 let providers = self.leases.entry(lease.hash).or_default();
683 let update = match providers.get(&lease.provider) {
684 None => LeaseUpdate::Inserted,
685 Some(existing) if lease.sequence > existing.sequence => LeaseUpdate::Replaced,
686 Some(_) => LeaseUpdate::IgnoredOlder,
687 };
688 if update.changed() {
689 providers.insert(lease.provider, lease);
690 }
691 Ok(update)
692 }
693
694 pub fn record_lease(&mut self, lease: ProviderLease) -> Result<LeaseUpdate> {
700 self.track(lease)
701 }
702
703 pub fn add_lease(&mut self, lease: ProviderLease) -> Result<LeaseUpdate> {
709 self.track(lease)
710 }
711
712 pub fn invalidate_lease(&mut self, hash: Hash, provider: PublicKey) -> bool {
715 self.invalidate_lease_at(hash, provider, current_epoch_seconds())
716 }
717
718 pub fn invalidate_lease_at(&mut self, hash: Hash, provider: PublicKey, now: u64) -> bool {
721 let removed = self
722 .leases
723 .get_mut(&hash)
724 .is_some_and(|providers| providers.remove(&provider).is_some());
725 if let Some(providers) = self.leases.get(&hash)
726 && providers.is_empty()
727 {
728 self.leases.remove(&hash);
729 }
730 self.ban_provider(
731 provider,
732 Some(hash),
733 "provider failed to serve the leased blob".to_owned(),
734 BanSource::Automated,
735 Some(AUTOMATED_BAN_DURATION),
736 now,
737 );
738 removed
739 }
740
741 #[must_use]
744 pub fn is_banned(&self, provider: PublicKey, hash: &Hash, now: u64) -> bool {
745 self.bans.get(&provider).is_some_and(|ban| ban_is_active(ban, now))
746 || self
747 .hash_bans
748 .get(hash)
749 .and_then(|providers| providers.get(&provider))
750 .is_some_and(|ban| ban_is_active(ban, now))
751 }
752
753 pub fn ban_provider(
755 &mut self,
756 provider: PublicKey,
757 scope: Option<Hash>,
758 reason: impl Into<String>,
759 source: BanSource,
760 duration: Option<Duration>,
761 now: u64,
762 ) -> BanRecord {
763 let record = BanRecord {
764 provider,
765 hash: scope,
766 banned_at: now,
767 expires_at: duration.map(|value| now.saturating_add(value.as_secs())),
768 reason: reason.into(),
769 source,
770 };
771 match scope {
772 Some(scope_hash) => {
773 let bans = self.hash_bans.entry(scope_hash).or_default();
774 insert_ban(bans, record.clone());
775 }
776 None => {
777 insert_ban(&mut self.bans, record.clone());
778 }
779 }
780 record
781 }
782
783 pub fn unban_provider(&mut self, provider: PublicKey, scope: Option<Hash>) -> bool {
785 match scope {
786 Some(scope_hash) => {
787 let removed = self
788 .hash_bans
789 .get_mut(&scope_hash)
790 .is_some_and(|providers| providers.remove(&provider).is_some());
791 if self.hash_bans.get(&scope_hash).is_some_and(HashMap::is_empty) {
792 self.hash_bans.remove(&scope_hash);
793 }
794 removed
795 }
796 None => self.bans.remove(&provider).is_some(),
797 }
798 }
799
800 #[must_use]
803 pub fn banned_providers(&self, hash: &Hash, now: u64) -> Vec<PublicKey> {
804 let mut providers = self
805 .bans
806 .values()
807 .filter(|ban| ban_is_active(ban, now))
808 .map(|ban| ban.provider)
809 .collect::<Vec<_>>();
810 if let Some(scoped) = self.hash_bans.get(hash) {
811 providers.extend(
812 scoped
813 .values()
814 .filter(|ban| ban_is_active(ban, now))
815 .map(|ban| ban.provider),
816 );
817 }
818 providers.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
819 providers.dedup();
820 providers
821 }
822
823 #[must_use]
825 pub fn ban_records(&self, hash: &Hash, now: u64) -> Vec<BanRecord> {
826 let mut records = Vec::new();
827 records.extend(self.bans.values().filter(|ban| ban_is_active(ban, now)).cloned());
828 if let Some(scoped) = self.hash_bans.get(hash) {
829 records.extend(scoped.values().filter(|ban| ban_is_active(ban, now)).cloned());
830 }
831 records.sort_by(|left, right| left.provider.as_bytes().cmp(right.provider.as_bytes()));
832 records
833 }
834
835 pub fn retroactive_invalidate(&mut self, hash: Hash, successful_provider: PublicKey, now: u64) -> Vec<PublicKey> {
838 self.purge_stale_failures(now, DEFAULT_FAILURE_TTL);
839 let failed_providers = self
840 .failures
841 .get(&hash)
842 .map(|providers| {
843 providers
844 .iter()
845 .filter(|(provider, record)| {
846 **provider != successful_provider
847 && record.failures.iter().any(|failure| failure.kind.is_definitive())
848 })
849 .map(|(provider, _)| *provider)
850 .collect::<Vec<_>>()
851 })
852 .unwrap_or_default();
853 let mut invalidated = failed_providers
854 .into_iter()
855 .filter(|provider| self.invalidate_lease_at(hash, *provider, now))
856 .collect::<Vec<_>>();
857 invalidated.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
858 invalidated
859 }
860
861 pub fn record_failure(&mut self, hash: Hash, provider: PublicKey, failure: FetchFailure) {
863 self.record_failure_at(hash, provider, failure, current_epoch_seconds());
864 }
865
866 pub fn record_failure_at(&mut self, hash: Hash, provider: PublicKey, failure: FetchFailure, now: u64) {
871 self.purge_stale_failures(now, DEFAULT_FAILURE_TTL);
872 let existing_total = self
873 .failure_totals
874 .get(&hash)
875 .and_then(|providers| providers.get(&provider))
876 .copied()
877 .unwrap_or(0);
878 let previous_kind = self
879 .failures
880 .get(&hash)
881 .and_then(|providers| providers.get(&provider))
882 .and_then(|record| record.failures.last())
883 .map(|item| item.kind);
884 let previous_consecutive = self
885 .failures
886 .get(&hash)
887 .and_then(|providers| providers.get(&provider))
888 .map_or(0, |record| record.consecutive_failures);
889 let record = self
890 .failures
891 .entry(hash)
892 .or_default()
893 .entry(provider)
894 .or_insert_with(|| FailureRecord {
895 failures: Vec::new(),
896 consecutive_failures: 0,
897 last_failure_at: now,
898 first_failure_at: now,
899 });
900 if existing_total == 0 {
901 record.first_failure_at = now;
902 }
903 record.last_failure_at = now;
904 record.consecutive_failures = previous_consecutive.saturating_add(1);
905 let definitive_streak = self
906 .definitive_streaks
907 .entry(hash)
908 .or_default()
909 .entry(provider)
910 .or_default();
911 *definitive_streak = if failure.kind.is_definitive() {
912 if previous_kind.is_some_and(FetchFailureKind::is_definitive) {
913 definitive_streak.saturating_add(1)
914 } else {
915 1
916 }
917 } else {
918 0
919 };
920 record.failures.push(failure);
921 let keep_from = record.failures.len().saturating_sub(self.max_failures_per_provider);
922 if keep_from > 0 {
923 record.failures.drain(..keep_from);
924 }
925 let total = self
926 .failure_totals
927 .entry(hash)
928 .or_default()
929 .entry(provider)
930 .or_default();
931 *total = existing_total.saturating_add(1);
932 }
933
934 #[must_use]
936 pub fn failure_record(&self, hash: &Hash, provider: &PublicKey) -> Option<&FailureRecord> {
937 self.failures.get(hash).and_then(|providers| providers.get(provider))
938 }
939
940 #[must_use]
942 pub fn failure_count(&self, hash: &Hash, provider: &PublicKey) -> usize {
943 self.failure_totals
944 .get(hash)
945 .and_then(|providers| providers.get(provider))
946 .copied()
947 .map_or(0, |count| usize::try_from(count).unwrap_or(usize::MAX))
948 }
949
950 #[must_use]
952 pub fn consecutive_failures(&self, hash: &Hash, provider: &PublicKey) -> u32 {
953 self.failure_record(hash, provider)
954 .map_or(0, |record| record.consecutive_failures)
955 }
956
957 #[must_use]
959 pub fn is_definitively_failed(&self, hash: &Hash, provider: &PublicKey) -> bool {
960 self.definitive_streaks
961 .get(hash)
962 .and_then(|providers| providers.get(provider))
963 .is_some_and(|streak| *streak >= DEFINITIVE_FAILURE_THRESHOLD)
964 }
965
966 pub fn clear_failures_for_provider(&mut self, hash: &Hash, provider: &PublicKey) {
968 if let Some(providers) = self.failures.get_mut(hash) {
969 providers.remove(provider);
970 if providers.is_empty() {
971 self.failures.remove(hash);
972 }
973 }
974 if let Some(providers) = self.failure_totals.get_mut(hash) {
975 providers.remove(provider);
976 if providers.is_empty() {
977 self.failure_totals.remove(hash);
978 }
979 }
980 if let Some(providers) = self.definitive_streaks.get_mut(hash) {
981 providers.remove(provider);
982 if providers.is_empty() {
983 self.definitive_streaks.remove(hash);
984 }
985 }
986 }
987
988 pub fn purge_stale_failures(&mut self, now: u64, ttl: Duration) {
990 let ttl_seconds = ttl.as_secs();
991 let mut stale = Vec::new();
992 self.failures.retain(|hash, providers| {
993 providers.retain(|provider, record| {
994 let keep = now.saturating_sub(record.last_failure_at) <= ttl_seconds;
995 if !keep {
996 stale.push((*hash, *provider));
997 }
998 keep
999 });
1000 !providers.is_empty()
1001 });
1002 for (hash, provider) in stale {
1003 if let Some(totals) = self.failure_totals.get_mut(&hash) {
1004 totals.remove(&provider);
1005 }
1006 if let Some(streaks) = self.definitive_streaks.get_mut(&hash) {
1007 streaks.remove(&provider);
1008 }
1009 }
1010 self.failure_totals.retain(|_, providers| !providers.is_empty());
1011 self.definitive_streaks.retain(|_, providers| !providers.is_empty());
1012 }
1013
1014 pub fn observe_provider(&mut self, hash: Hash, provider: PublicKey) {
1016 self.observe_provider_at(hash, provider, current_epoch_seconds());
1017 }
1018
1019 pub fn observe_provider_at(&mut self, hash: Hash, provider: PublicKey, observed_at: u64) {
1021 self.observations.entry(hash).or_default().insert(provider, observed_at);
1022 }
1023
1024 #[must_use]
1026 pub fn leases(&self, hash: &Hash) -> Vec<ProviderLease> {
1027 self.leases_at(hash, current_epoch_seconds())
1028 }
1029
1030 #[must_use]
1032 pub fn leases_at(&self, hash: &Hash, now: u64) -> Vec<ProviderLease> {
1033 let mut leases = self
1034 .leases
1035 .get(hash)
1036 .map(|providers| {
1037 providers
1038 .values()
1039 .filter(|lease| !lease.is_expired_at(now) && !self.is_banned(lease.provider, hash, now))
1040 .cloned()
1041 .collect::<Vec<_>>()
1042 })
1043 .unwrap_or_default();
1044 leases.sort_by(|left, right| left.provider.as_bytes().cmp(right.provider.as_bytes()));
1045 leases
1046 }
1047
1048 #[must_use]
1050 pub fn providers(&self, hash: &Hash) -> Vec<PublicKey> {
1051 self.leases(hash).into_iter().map(|lease| lease.provider).collect()
1052 }
1053
1054 #[must_use]
1056 pub fn verified_count(&self, hash: &Hash) -> usize {
1057 self.leases(hash).len()
1058 }
1059
1060 #[must_use]
1062 pub fn health(&self, hash: &Hash, observation_ttl: Duration) -> AvailabilityHealth {
1063 self.health_at(hash, current_epoch_seconds(), observation_ttl)
1064 }
1065
1066 #[must_use]
1068 pub fn health_at(&self, hash: &Hash, now: u64, observation_ttl: Duration) -> AvailabilityHealth {
1069 let verified_providers = self
1070 .leases_at(hash, now)
1071 .into_iter()
1072 .map(|lease| lease.provider)
1073 .collect::<Vec<_>>();
1074 let ttl = observation_ttl.as_secs();
1075 let mut local_providers = self
1076 .observations
1077 .get(hash)
1078 .map(|observations| {
1079 observations
1080 .iter()
1081 .filter(|(_, observed_at)| now.saturating_sub(**observed_at) <= ttl)
1082 .map(|(provider, _)| *provider)
1083 .collect::<Vec<_>>()
1084 })
1085 .unwrap_or_default();
1086 local_providers.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
1087 AvailabilityHealth {
1088 verified: verified_providers.len(),
1089 local: local_providers.len(),
1090 verified_providers,
1091 local_providers,
1092 }
1093 }
1094
1095 pub fn purge(&mut self, now: u64, observation_ttl: Duration) {
1097 self.leases.retain(|_, providers| {
1098 providers.retain(|_, lease| !lease.is_expired_at(now));
1099 !providers.is_empty()
1100 });
1101 let ttl = observation_ttl.as_secs();
1102 self.observations.retain(|_, providers| {
1103 providers.retain(|_, observed_at| now.saturating_sub(*observed_at) <= ttl);
1104 !providers.is_empty()
1105 });
1106 self.purge_stale_failures(now, DEFAULT_FAILURE_TTL);
1107 }
1108}
1109
1110fn ban_is_active(ban: &BanRecord, now: u64) -> bool {
1111 ban.expires_at.is_none_or(|expires_at| expires_at > now)
1112}
1113
1114const fn ban_priority(source: BanSource) -> u8 {
1115 match source {
1116 BanSource::Automated => 0,
1117 BanSource::WoT => 1,
1118 BanSource::Manual => 2,
1119 }
1120}
1121
1122fn insert_ban(bans: &mut HashMap<PublicKey, BanRecord>, record: BanRecord) {
1123 let should_replace = bans.get(&record.provider).is_none_or(|existing| {
1124 ban_priority(record.source) >= ban_priority(existing.source) || !ban_is_active(existing, record.banned_at)
1125 });
1126 if should_replace {
1127 bans.insert(record.provider, record);
1128 }
1129}
1130
1131#[derive(Clone, Debug, Eq, PartialEq)]
1133#[non_exhaustive]
1134pub struct ReplicationBudget {
1135 pub min_providers: usize,
1136 pub responsible_peers: usize,
1137 pub max_jitter: Duration,
1138 pub observation_ttl: Duration,
1139}
1140
1141impl ReplicationBudget {
1142 #[must_use]
1144 pub const fn new(min_providers: usize) -> Self {
1145 Self {
1146 min_providers,
1147 responsible_peers: DEFAULT_RESPONSIBLE_PEERS,
1148 max_jitter: DEFAULT_MAX_JITTER,
1149 observation_ttl: DEFAULT_OBSERVATION_TTL,
1150 }
1151 }
1152
1153 #[must_use]
1155 pub const fn with_responsible_peers(mut self, peers: usize) -> Self {
1156 self.responsible_peers = peers;
1157 self
1158 }
1159
1160 #[must_use]
1162 pub const fn with_max_jitter(mut self, jitter: Duration) -> Self {
1163 self.max_jitter = jitter;
1164 self
1165 }
1166
1167 #[must_use]
1169 pub const fn with_observation_ttl(mut self, ttl: Duration) -> Self {
1170 self.observation_ttl = ttl;
1171 self
1172 }
1173}
1174
1175impl Default for ReplicationBudget {
1176 fn default() -> Self {
1177 Self::new(3)
1178 }
1179}
1180
1181#[derive(Clone, Debug, Eq, PartialEq)]
1183#[non_exhaustive]
1184pub struct ResilienceConfig {
1185 pub budget: ReplicationBudget,
1186 pub max_failures_per_provider: usize,
1187}
1188
1189impl ResilienceConfig {
1190 #[must_use]
1191 pub const fn new(budget: ReplicationBudget) -> Self {
1192 Self {
1193 budget,
1194 max_failures_per_provider: DEFAULT_MAX_FAILURES_PER_PROVIDER,
1195 }
1196 }
1197
1198 #[must_use]
1200 pub const fn with_max_failures_per_provider(mut self, max_failures_per_provider: usize) -> Self {
1201 self.max_failures_per_provider = max_failures_per_provider;
1202 self
1203 }
1204}
1205
1206impl Default for ResilienceConfig {
1207 fn default() -> Self {
1208 Self::new(ReplicationBudget::default())
1209 }
1210}
1211
1212#[derive(Clone, Debug, Eq, PartialEq)]
1214#[non_exhaustive]
1215pub struct ReplicationResult {
1216 pub hash: Hash,
1217 pub health_before: AvailabilityHealth,
1218 pub health_after: AvailabilityHealth,
1219 pub selected_providers: Vec<PublicKey>,
1220 pub fetched_from: Vec<PublicKey>,
1221 pub failed_from: Vec<(PublicKey, FetchFailureKind)>,
1222 pub invalidated_leases: Vec<PublicKey>,
1223 pub pinned: bool,
1224 pub short_circuited: bool,
1225}
1226
1227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1229#[non_exhaustive]
1230pub enum FetchWait {
1231 Ready,
1232 ShortCircuited,
1233}
1234
1235#[derive(Clone)]
1237pub struct ResilienceService {
1238 config: ResilienceConfig,
1239 tracker: Arc<Mutex<ProviderLeaseTracker>>,
1240 generations: Arc<Mutex<HashMap<Hash, watch::Sender<u64>>>>,
1241 reputation: Arc<Mutex<ProviderReputationStore>>,
1242 wot: Option<WotService>,
1243}
1244
1245impl std::fmt::Debug for ResilienceService {
1246 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1247 formatter
1248 .debug_struct("ResilienceService")
1249 .field("config", &self.config)
1250 .finish_non_exhaustive()
1251 }
1252}
1253
1254impl ResilienceService {
1255 #[must_use]
1257 pub fn new(config: ResilienceConfig) -> Self {
1258 let tracker = ProviderLeaseTracker::with_max_failures_per_provider(config.max_failures_per_provider);
1259 Self {
1260 config,
1261 tracker: Arc::new(Mutex::new(tracker)),
1262 generations: Arc::new(Mutex::new(HashMap::new())),
1263 reputation: Arc::new(Mutex::new(ProviderReputationStore::default())),
1264 wot: None,
1265 }
1266 }
1267
1268 #[must_use]
1270 pub fn with_budget(budget: ReplicationBudget) -> Self {
1271 Self::new(ResilienceConfig::new(budget))
1272 }
1273
1274 #[must_use]
1276 pub fn with_wot(config: ResilienceConfig, wot: WotService) -> Self {
1277 let mut service = Self::new(config);
1278 service.wot = Some(wot);
1279 service
1280 }
1281
1282 #[must_use]
1284 pub fn with_reputation(
1285 config: ResilienceConfig,
1286 reputation_config: ReputationConfig,
1287 wot: Option<WotService>,
1288 ) -> Self {
1289 let mut service = Self::new(config);
1290 service.reputation = Arc::new(Mutex::new(ProviderReputationStore::new(reputation_config)));
1291 service.wot = wot;
1292 service
1293 }
1294
1295 #[must_use]
1296 pub const fn config(&self) -> &ResilienceConfig {
1297 &self.config
1298 }
1299
1300 #[must_use]
1302 pub fn reputation_store(&self) -> Arc<Mutex<ProviderReputationStore>> {
1303 Arc::clone(&self.reputation)
1304 }
1305
1306 #[must_use]
1308 pub fn with_trust_policy(mut self, wot: WotService) -> Self {
1309 self.wot = Some(wot);
1310 self
1311 }
1312
1313 pub fn record_lease(&self, lease: ProviderLease) -> Result<LeaseUpdate> {
1320 let hash = lease.hash;
1321 let update = self
1322 .tracker
1323 .lock()
1324 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1325 .track(lease)?;
1326 if update.changed() {
1327 self.bump_generation(hash)?;
1328 }
1329 Ok(update)
1330 }
1331
1332 pub fn track_lease(&self, lease: ProviderLease) -> Result<LeaseUpdate> {
1339 self.record_lease(lease)
1340 }
1341
1342 pub fn ingest_lease(&self, lease: ProviderLease) -> Result<LeaseUpdate> {
1349 self.record_lease(lease)
1350 }
1351
1352 pub fn invalidate_provider(&self, hash: Hash, provider: PublicKey) -> Result<bool> {
1359 let invalidated = self
1360 .tracker
1361 .lock()
1362 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1363 .invalidate_lease(hash, provider);
1364 if invalidated {
1365 self.bump_generation(hash)?;
1366 }
1367 Ok(invalidated)
1368 }
1369
1370 pub fn ban_provider(
1376 &self,
1377 provider: PublicKey,
1378 reason: impl Into<String>,
1379 scope: Option<Hash>,
1380 duration: Option<Duration>,
1381 ) -> Result<BanRecord> {
1382 let record = self
1383 .tracker
1384 .lock()
1385 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1386 .ban_provider(
1387 provider,
1388 scope,
1389 reason,
1390 BanSource::Manual,
1391 duration,
1392 current_epoch_seconds(),
1393 );
1394 if let Some(scope_hash) = scope {
1395 self.bump_generation(scope_hash)?;
1396 }
1397 Ok(record)
1398 }
1399
1400 pub fn unban_provider(&self, provider: PublicKey, scope: Option<Hash>) -> Result<bool> {
1406 let removed = self
1407 .tracker
1408 .lock()
1409 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1410 .unban_provider(provider, scope);
1411 if removed && let Some(scope_hash) = scope {
1412 self.bump_generation(scope_hash)?;
1413 }
1414 Ok(removed)
1415 }
1416
1417 pub fn retroactive_invalidate(&self, hash: Hash, successful_provider: PublicKey) -> Result<Vec<PublicKey>> {
1424 let invalidated = self
1425 .tracker
1426 .lock()
1427 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1428 .retroactive_invalidate(hash, successful_provider, current_epoch_seconds());
1429 if !invalidated.is_empty() {
1430 self.bump_generation(hash)?;
1431 }
1432 Ok(invalidated)
1433 }
1434
1435 pub fn observe_provider(&self, hash: Hash, provider: PublicKey) -> Result<()> {
1441 self.tracker
1442 .lock()
1443 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1444 .observe_provider(hash, provider);
1445 Ok(())
1446 }
1447
1448 pub fn health(&self, hash: &Hash) -> Result<AvailabilityHealth> {
1454 Ok(self
1455 .tracker
1456 .lock()
1457 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1458 .health(hash, self.config.budget.observation_ttl))
1459 }
1460
1461 pub fn needs_replication(&self, hash: &Hash) -> Result<bool> {
1467 Ok(self.health(hash)?.verified < self.config.budget.min_providers)
1468 }
1469
1470 pub fn responsible_providers(&self, hash: &Hash) -> Result<Vec<PublicKey>> {
1476 let providers = self
1477 .tracker
1478 .lock()
1479 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1480 .providers(hash);
1481 Ok(self
1482 .rank_provider_candidates(*hash, &providers, current_epoch_seconds())?
1483 .into_iter()
1484 .take(self.config.budget.responsible_peers)
1485 .collect())
1486 }
1487
1488 pub fn record_failure(&self, hash: Hash, provider: PublicKey, failure: FetchFailure) -> Result<()> {
1494 self.record_failure_at(hash, provider, failure, current_epoch_seconds())
1495 }
1496
1497 pub fn record_failure_at(&self, hash: Hash, provider: PublicKey, failure: FetchFailure, now: u64) -> Result<()> {
1503 let kind = failure.kind;
1504 self.tracker
1505 .lock()
1506 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1507 .record_failure_at(hash, provider, failure, now);
1508 self.reputation
1509 .lock()
1510 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?
1511 .record_failure(provider, kind, now);
1512 Ok(())
1513 }
1514
1515 pub fn clear_failures_for_provider(&self, hash: &Hash, provider: &PublicKey) -> Result<()> {
1521 self.tracker
1522 .lock()
1523 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1524 .clear_failures_for_provider(hash, provider);
1525 Ok(())
1526 }
1527
1528 pub fn record_success(&self, hash: &Hash, provider: &PublicKey) -> Result<()> {
1535 self.clear_failures_for_provider(hash, provider)?;
1536 self.reputation
1537 .lock()
1538 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?
1539 .record_success(*provider, current_epoch_seconds());
1540 Ok(())
1541 }
1542
1543 pub fn reputation_score(&self, provider: PublicKey) -> Result<f64> {
1549 Ok(self
1550 .reputation
1551 .lock()
1552 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?
1553 .score(provider, current_epoch_seconds()))
1554 }
1555
1556 #[must_use]
1558 pub fn jitter_delay(&self, hash: Hash, provider: PublicKey) -> Duration {
1559 jitter_delay(hash, provider, self.config.budget.max_jitter)
1560 }
1561
1562 pub async fn wait_for_fetch_slot(&self, hash: Hash, provider: PublicKey) -> Result<FetchWait> {
1569 let mut generation = self.generation_receiver(hash)?;
1570 let delay = self.jitter_delay(hash, provider);
1571 tokio::select! {
1572 () = tokio::time::sleep(delay) => Ok(FetchWait::Ready),
1573 changed = generation.changed() => {
1574 changed.map_err(|error| SyncwebError::operation("provider lease cancellation channel closed", error))?;
1575 Ok(FetchWait::ShortCircuited)
1576 }
1577 }
1578 }
1579
1580 pub async fn ensure_replication(
1592 &self,
1593 endpoint: &Endpoint,
1594 blobs: &BlobStore,
1595 hash: Hash,
1596 ) -> Result<ReplicationResult> {
1597 let health_before = self.health(&hash)?;
1598 let mut result = ReplicationResult {
1599 hash,
1600 health_before: health_before.clone(),
1601 health_after: health_before.clone(),
1602 selected_providers: Vec::new(),
1603 fetched_from: Vec::new(),
1604 failed_from: Vec::new(),
1605 invalidated_leases: Vec::new(),
1606 pinned: false,
1607 short_circuited: false,
1608 };
1609 if health_before.verified >= self.config.budget.min_providers {
1610 return Ok(result);
1611 }
1612
1613 let now = current_epoch_seconds();
1614 let leases = self
1615 .tracker
1616 .lock()
1617 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1618 .leases_at(&hash, now);
1619 let providers = leases.iter().map(|lease| lease.provider).collect::<Vec<_>>();
1620 result.selected_providers = self.rank_provider_candidates(hash, &providers, now)?;
1621 result.selected_providers.truncate(self.config.budget.responsible_peers);
1622 if result.selected_providers.is_empty() {
1623 return Ok(result);
1624 }
1625
1626 if blobs.has(hash).await? {
1627 blobs.pin(replication_pin_name(hash), hash).await?;
1628 result.pinned = true;
1629 self.observe_provider(hash, endpoint.secret_key().public())?;
1630 result.health_after = self.health(&hash)?;
1631 return Ok(result);
1632 }
1633 let selected = result.selected_providers.clone();
1634 for provider in selected {
1635 if self
1636 .tracker
1637 .lock()
1638 .map_err(|error| SyncwebError::operation("provider lease tracker lock poisoned", error))?
1639 .is_banned(provider, &hash, current_epoch_seconds())
1640 {
1641 continue;
1642 }
1643 if self
1644 .reputation
1645 .lock()
1646 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?
1647 .is_banned(provider, current_epoch_seconds())
1648 {
1649 continue;
1650 }
1651 match self.wait_for_fetch_slot(hash, provider).await? {
1652 FetchWait::ShortCircuited => {
1653 result.short_circuited = true;
1654 result.health_after = self.health(&hash)?;
1655 return Ok(result);
1656 }
1657 FetchWait::Ready => {}
1658 }
1659 let Some(lease) = leases.iter().find(|lease| lease.provider == provider) else {
1660 continue;
1661 };
1662 let ticket = parse_ticket(&lease.ticket)?;
1663 match blobs.fetch(endpoint, &ticket).await {
1664 Ok(()) => {
1665 result.fetched_from.push(provider);
1666 self.reputation
1667 .lock()
1668 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?
1669 .record_success(provider, current_epoch_seconds());
1670 self.clear_failures_for_provider(&hash, &provider)?;
1671 result
1672 .invalidated_leases
1673 .extend(self.retroactive_invalidate(hash, provider)?);
1674 break;
1675 }
1676 Err(error) => {
1677 let kind = FetchFailureKind::from_syncweb_error(&error);
1678 let failure = FetchFailure::from_syncweb_error(provider, hash, &error);
1679 self.record_failure(hash, provider, failure)?;
1680 result.failed_from.push((provider, kind));
1681 if kind.is_definitive() && self.invalidate_provider(hash, provider)? {
1682 result.invalidated_leases.push(provider);
1683 }
1684 }
1685 }
1686 }
1687 if result.fetched_from.is_empty() {
1688 result.health_after = self.health(&hash)?;
1689 return Ok(result);
1690 }
1691
1692 blobs.pin(replication_pin_name(hash), hash).await?;
1693 result.pinned = true;
1694 self.observe_provider(hash, endpoint.secret_key().public())?;
1695 result.health_after = self.health(&hash)?;
1696 Ok(result)
1697 }
1698
1699 fn rank_provider_candidates(&self, hash: Hash, providers: &[PublicKey], now: u64) -> Result<Vec<PublicKey>> {
1700 let mut candidates = {
1701 let reputation = self
1702 .reputation
1703 .lock()
1704 .map_err(|error| SyncwebError::operation("provider reputation lock poisoned", error))?;
1705 reputation
1706 .rank_provider_list(now, hash, providers)
1707 .into_iter()
1708 .filter(|provider| !reputation.is_banned(*provider, now))
1709 .map(|provider| (reputation.score(provider, now), provider))
1710 .collect::<Vec<_>>()
1711 };
1712 if let Some(wot) = &self.wot {
1713 let mut trusted = Vec::with_capacity(candidates.len());
1714 for (score, provider) in candidates {
1715 let decision = wot.evaluate_provider_trust(provider, Some(&hash), now)?;
1716 if decision == ProviderTrustDecision::Distrusted {
1717 continue;
1718 }
1719 trusted.push((matches!(decision, ProviderTrustDecision::Trusted), score, provider));
1720 }
1721 trusted.sort_by(|left, right| {
1722 right
1723 .0
1724 .cmp(&left.0)
1725 .then_with(|| right.1.total_cmp(&left.1))
1726 .then_with(|| xor_distance(hash, left.2).cmp(&xor_distance(hash, right.2)))
1727 .then_with(|| left.2.as_bytes().cmp(right.2.as_bytes()))
1728 });
1729 return Ok(trusted.into_iter().map(|(_, _, provider)| provider).collect());
1730 }
1731 candidates.sort_by(|left, right| {
1732 right
1733 .0
1734 .total_cmp(&left.0)
1735 .then_with(|| xor_distance(hash, left.1).cmp(&xor_distance(hash, right.1)))
1736 .then_with(|| left.1.as_bytes().cmp(right.1.as_bytes()))
1737 });
1738 Ok(candidates.into_iter().map(|(_, provider)| provider).collect())
1739 }
1740
1741 pub async fn replicate(&self, endpoint: &Endpoint, blobs: &BlobStore, hash: Hash) -> Result<ReplicationResult> {
1747 self.ensure_replication(endpoint, blobs, hash).await
1748 }
1749
1750 pub async fn subscribe(&self, gossip: &GossipService, bootstrap: Vec<PublicKey>) -> Result<GossipTopic> {
1756 gossip.subscribe(resilience_topic(), bootstrap).await
1757 }
1758
1759 pub async fn announce(&self, gossip: &GossipService, sender: &GossipSender, lease: &ProviderLease) -> Result<()> {
1765 lease.verify_at(current_epoch_seconds())?;
1766 gossip.publish(sender, lease.to_bytes()?).await
1767 }
1768
1769 pub async fn consume_gossip(&self, topic: &mut GossipTopic, timeout: Duration) -> Result<usize> {
1778 let mut tracked = 0_usize;
1779 let receive = async {
1780 while let Some(event) = topic.next().await {
1781 if let Event::Received(message) =
1782 event.map_err(|error| SyncwebError::operation("provider lease gossip event failed", error))?
1783 {
1784 let lease = ProviderLease::from_bytes(message.content)?;
1785 self.record_lease(lease)?;
1786 tracked = tracked.saturating_add(1);
1787 }
1788 }
1789 Ok::<(), SyncwebError>(())
1790 };
1791 if let Ok(result) = tokio::time::timeout(timeout, receive).await {
1792 result?;
1793 }
1794 Ok(tracked)
1795 }
1796
1797 #[must_use]
1799 pub fn spawn_gossip_listener(&self, mut topic: GossipTopic) -> JoinHandle<Result<usize>> {
1800 let service = self.clone();
1801 tokio::spawn(async move { service.consume_gossip(&mut topic, Duration::MAX).await })
1802 }
1803
1804 fn generation_receiver(&self, hash: Hash) -> Result<watch::Receiver<u64>> {
1805 let mut generations = self
1806 .generations
1807 .lock()
1808 .map_err(|error| SyncwebError::operation("provider lease generation lock poisoned", error))?;
1809 Ok(generations
1810 .entry(hash)
1811 .or_insert_with(|| watch::channel(0_u64).0)
1812 .subscribe())
1813 }
1814
1815 fn bump_generation(&self, hash: Hash) -> Result<()> {
1816 {
1817 let mut generations = self
1818 .generations
1819 .lock()
1820 .map_err(|error| SyncwebError::operation("provider lease generation lock poisoned", error))?;
1821 let sender = generations.entry(hash).or_insert_with(|| watch::channel(0_u64).0);
1822 sender.send_modify(|generation| *generation = generation.saturating_add(1));
1823 drop(generations);
1824 }
1825 Ok(())
1826 }
1827}
1828
1829#[must_use]
1831pub fn resilience_topic() -> TopicId {
1832 TopicId::from_bytes(*blake3::hash(RESILIENCE_TOPIC_SEED).as_bytes())
1833}
1834
1835#[must_use]
1840pub fn consistent_hashing_selection(hash: Hash, providers: &[PublicKey], count: usize) -> Vec<PublicKey> {
1841 if count == 0 {
1842 return Vec::new();
1843 }
1844 let mut unique = HashSet::new();
1845 let mut ranked = providers
1846 .iter()
1847 .copied()
1848 .filter(|provider| unique.insert(*provider))
1849 .map(|provider| (xor_distance(hash, provider), provider))
1850 .collect::<Vec<_>>();
1851 ranked.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.as_bytes().cmp(right.1.as_bytes())));
1852 ranked.truncate(count);
1853 ranked.into_iter().map(|(_, provider)| provider).collect()
1854}
1855
1856#[must_use]
1858pub fn xor_distance(hash: Hash, provider: PublicKey) -> [u8; 32] {
1859 let mut distance = [0_u8; 32];
1860 for ((byte, hash_byte), provider_byte) in distance.iter_mut().zip(hash.as_bytes()).zip(provider.as_bytes()) {
1861 *byte = *hash_byte ^ *provider_byte;
1862 }
1863 distance
1864}
1865
1866#[must_use]
1872pub fn jitter_delay(hash: Hash, provider: PublicKey, max: Duration) -> Duration {
1873 let max_millis = u64::try_from(max.as_millis()).unwrap_or(u64::MAX);
1874 if max_millis == 0 {
1875 return Duration::ZERO;
1876 }
1877 let mut seed = Vec::with_capacity(64);
1878 seed.extend_from_slice(hash.as_bytes());
1879 seed.extend_from_slice(provider.as_bytes());
1880 let digest = blake3::hash(&seed);
1881 let mut value_bytes = [0_u8; 8];
1882 for (target, source) in value_bytes.iter_mut().zip(digest.as_bytes().iter().take(8)) {
1883 *target = *source;
1884 }
1885 let value = u64::from_le_bytes(value_bytes);
1886 let divisor = max_millis.saturating_add(1);
1887 Duration::from_millis(value.checked_rem(divisor).unwrap_or(0))
1888}
1889
1890fn replication_pin_name(hash: Hash) -> String {
1891 format!("{REPLICATION_PIN_PREFIX}{hash}")
1892}
1893
1894fn parse_ticket(ticket: &str) -> Result<BlobTicket> {
1895 BlobTicket::from_str(ticket).map_err(|error| SyncwebError::InvalidTicket(error.to_string()))
1896}
1897
1898fn current_epoch_seconds() -> u64 {
1899 SystemTime::now()
1900 .duration_since(UNIX_EPOCH)
1901 .map_or(0, |duration| duration.as_secs())
1902}
1903
1904#[cfg(test)]
1905mod tests {
1906 use std::{io::Cursor, time::Duration};
1907
1908 use anyhow::Result;
1909 use iroh::{EndpointAddr, SecretKey};
1910 use iroh_blobs::{BlobFormat, Hash};
1911
1912 use super::*;
1913
1914 fn signed_lease(seed: u8, hash: Hash, sequence: u64) -> Result<(ProviderLease, SecretKey)> {
1915 let secret_key = SecretKey::from_bytes(&[seed; 32]);
1916 let ticket = BlobTicket::new(EndpointAddr::new(secret_key.public()), hash, BlobFormat::Raw).to_string();
1917 let mut lease = ProviderLease::new_with_times(hash, ticket, sequence, 0, u64::MAX)?;
1918 lease.sign_with_secret_key(&secret_key)?;
1919 Ok((lease, secret_key))
1920 }
1921
1922 #[test]
1923 fn provider_leases_are_signed_and_monotonic() -> Result<()> {
1924 let hash = Hash::from_bytes([7_u8; 32]);
1925 let (lease, provider_key) = signed_lease(1, hash, 1)?;
1926 let encoded = lease.to_bytes()?;
1927 let decoded = ProviderLease::from_bytes(encoded)?;
1928 decoded.verify_at(10)?;
1929
1930 let mut tracker = ProviderLeaseTracker::default();
1931 anyhow::ensure!(
1932 tracker.track_at(decoded.clone(), 10)? == LeaseUpdate::Inserted,
1933 "first lease should be inserted"
1934 );
1935 anyhow::ensure!(
1936 tracker.track_at(decoded, 10)? == LeaseUpdate::IgnoredOlder,
1937 "same sequence should be ignored"
1938 );
1939 let (replacement, _) = signed_lease(1, hash, 2)?;
1940 anyhow::ensure!(
1941 tracker.track_at(replacement, 10)? == LeaseUpdate::Replaced,
1942 "newer sequence should replace the old lease"
1943 );
1944 anyhow::ensure!(tracker.verified_count(&hash) == 1, "one provider should remain tracked");
1945 tracker.observe_provider_at(hash, provider_key.public(), 10);
1946 let health = tracker.health_at(&hash, 20, Duration::from_secs(30));
1947 anyhow::ensure!(health.verified == 1, "one verified lease should be reported");
1948 anyhow::ensure!(health.local == 1, "one local observation should be reported");
1949 Ok(())
1950 }
1951
1952 #[test]
1953 fn consistent_selection_and_jitter_are_stable() -> Result<()> {
1954 let hash = Hash::from_bytes([9_u8; 32]);
1955 let (_, first_key) = signed_lease(1, hash, 1)?;
1956 let (_, second_key) = signed_lease(2, hash, 1)?;
1957 let (_, third_key) = signed_lease(3, hash, 1)?;
1958 let providers = vec![third_key.public(), first_key.public(), second_key.public()];
1959 let selected = consistent_hashing_selection(hash, &providers, 2);
1960 anyhow::ensure!(
1961 selected == consistent_hashing_selection(hash, &providers, 2),
1962 "selection should be stable"
1963 );
1964 anyhow::ensure!(selected.len() == 2, "selection should honor its limit");
1965 anyhow::ensure!(
1966 selected.iter().all(|provider| providers.contains(provider)),
1967 "selection should only contain candidates"
1968 );
1969
1970 let first_delay = jitter_delay(hash, first_key.public(), Duration::from_millis(500));
1971 let second_delay = jitter_delay(hash, second_key.public(), Duration::from_millis(500));
1972 anyhow::ensure!(
1973 first_delay <= Duration::from_millis(500),
1974 "first jitter should stay within the configured window"
1975 );
1976 anyhow::ensure!(
1977 second_delay <= Duration::from_millis(500),
1978 "second jitter should stay within the configured window"
1979 );
1980 anyhow::ensure!(
1981 first_delay != second_delay,
1982 "providers should receive different jitter slots"
1983 );
1984 Ok(())
1985 }
1986
1987 #[tokio::test]
1988 async fn a_new_lease_short_circuits_a_pending_fetch() -> Result<()> {
1989 let hash = Hash::from_bytes([11_u8; 32]);
1990 let (first_lease, first_key) = signed_lease(1, hash, 1)?;
1991 let (second_lease, _) = signed_lease(2, hash, 1)?;
1992 let budget = ReplicationBudget::new(3).with_max_jitter(Duration::from_secs(1));
1993 let service = ResilienceService::with_budget(budget);
1994 service.record_lease(first_lease)?;
1995 let delay = service.jitter_delay(hash, first_key.public());
1996 if delay.is_zero() {
1997 return Ok(());
1998 }
1999 let waiting = service.clone();
2000 let task = tokio::spawn(async move { waiting.wait_for_fetch_slot(hash, first_key.public()).await });
2001 tokio::time::sleep(Duration::from_millis(10)).await;
2002 service.record_lease(second_lease)?;
2003 anyhow::ensure!(
2004 task.await
2005 .map_err(|error| SyncwebError::operation("fetch wait task failed", error))??
2006 == FetchWait::ShortCircuited,
2007 "new lease should cancel the pending fetch"
2008 );
2009 Ok(())
2010 }
2011
2012 #[test]
2013 fn health_verified_vs_local_are_distinct() -> Result<()> {
2014 let hash = Hash::from_bytes([13_u8; 32]);
2015 let (lease, key) = signed_lease(1, hash, 1)?;
2016
2017 let local_only = Hash::from_bytes([14_u8; 32]);
2018 let (_, local_key) = signed_lease(2, local_only, 1)?;
2019
2020 let service = ResilienceService::with_budget(ReplicationBudget::new(1));
2021 service.record_lease(lease.clone())?;
2022 service.observe_provider(local_only, local_key.public())?;
2023
2024 let health = service.health(&hash)?;
2025 anyhow::ensure!(health.verified == 1, "one verified lease should be present");
2026 anyhow::ensure!(health.local == 0, "no local observations for this hash");
2027 anyhow::ensure!(
2028 health.verified_providers == vec![key.public()],
2029 "verified providers should list the lease key"
2030 );
2031
2032 let local_health = service.health(&local_only)?;
2033 anyhow::ensure!(local_health.verified == 0, "no verified leases for local-only hash");
2034 anyhow::ensure!(local_health.local == 1, "one local observation should be present");
2035 anyhow::ensure!(
2036 local_health.local_providers == vec![local_key.public()],
2037 "local providers should list the observed key"
2038 );
2039
2040 anyhow::ensure!(
2041 !service.needs_replication(&hash)?,
2042 "min_providers=1 is satisfied by one verified lease"
2043 );
2044 anyhow::ensure!(
2045 service.needs_replication(&local_only)?,
2046 "hash with no verified leases should need replication"
2047 );
2048
2049 let high_budget = ResilienceService::with_budget(ReplicationBudget::new(3));
2050 high_budget.record_lease(lease)?;
2051 anyhow::ensure!(
2052 high_budget.needs_replication(&hash)?,
2053 "min_providers=3 is not satisfied by one verified lease"
2054 );
2055
2056 Ok(())
2057 }
2058
2059 #[test]
2060 fn purge_removes_expired_leases_and_stale_observations() -> Result<()> {
2061 let hash = Hash::from_bytes([15_u8; 32]);
2062 let sk1 = SecretKey::from_bytes(&[1; 32]);
2063 let ticket1 = BlobTicket::new(EndpointAddr::new(sk1.public()), hash, BlobFormat::Raw).to_string();
2064 let mut lease_a = ProviderLease::new_with_times(hash, ticket1, 1, 10, 50)?;
2065 lease_a.sign_with_secret_key(&sk1)?;
2066
2067 let sk2 = SecretKey::from_bytes(&[2; 32]);
2068 let ticket2 = BlobTicket::new(EndpointAddr::new(sk2.public()), hash, BlobFormat::Raw).to_string();
2069 let mut lease_b = ProviderLease::new_with_times(hash, ticket2, 1, 10, 200)?;
2070 lease_b.sign_with_secret_key(&sk2)?;
2071
2072 let mut tracker = ProviderLeaseTracker::default();
2073 tracker.track_at(lease_a, 10)?;
2074 tracker.track_at(lease_b, 10)?;
2075 tracker.observe_provider_at(hash, sk1.public(), 10);
2076 anyhow::ensure!(tracker.leases_at(&hash, 15).len() == 2, "both leases tracked");
2077
2078 tracker.purge(15, Duration::from_secs(5));
2079 anyhow::ensure!(tracker.leases_at(&hash, 15).len() == 2, "both active at t=15");
2080
2081 tracker.purge(55, Duration::from_secs(5));
2082 anyhow::ensure!(tracker.leases_at(&hash, 55).len() == 1, "lease_a expired at t=55");
2083
2084 tracker.purge(200, Duration::from_secs(5));
2085 anyhow::ensure!(tracker.leases_at(&hash, 200).is_empty(), "all expired at t=200");
2086
2087 Ok(())
2088 }
2089
2090 #[test]
2091 fn fetch_failure_kind_classification_and_definitiveness() {
2092 let not_found = SyncwebError::operation("fetch", "provider blob not found");
2093 let refused = SyncwebError::operation("fetch", "connection refused");
2094 let timeout = SyncwebError::operation("fetch", "request timed out");
2095 let corruption = SyncwebError::operation("fetch", "hash mismatch");
2096 let unknown = SyncwebError::operation("fetch", "unexpected provider response");
2097
2098 assert_eq!(
2099 FetchFailureKind::from_syncweb_error(¬_found),
2100 FetchFailureKind::NotFound
2101 );
2102 assert_eq!(
2103 FetchFailureKind::from_syncweb_error(&refused),
2104 FetchFailureKind::ConnectionRefused
2105 );
2106 assert_eq!(
2107 FetchFailureKind::from_syncweb_error(&timeout),
2108 FetchFailureKind::Timeout
2109 );
2110 assert_eq!(
2111 FetchFailureKind::from_syncweb_error(&corruption),
2112 FetchFailureKind::Corruption
2113 );
2114 assert_eq!(
2115 FetchFailureKind::from_syncweb_error(&unknown),
2116 FetchFailureKind::Unknown
2117 );
2118 assert!(FetchFailureKind::NotFound.is_definitive());
2119 assert!(FetchFailureKind::Corruption.is_definitive());
2120 assert!(FetchFailureKind::Timeout.is_transient());
2121 assert!(!FetchFailureKind::Timeout.is_definitive());
2122 }
2123
2124 #[test]
2125 fn fetch_failure_round_trip_and_timestamp() -> Result<()> {
2126 let hash = Hash::from_bytes([21_u8; 32]);
2127 let provider = SecretKey::from_bytes(&[21_u8; 32]).public();
2128 let before = current_epoch_seconds();
2129 let failure = FetchFailure::new(FetchFailureKind::Timeout, provider, hash, "timed out");
2130 let after = current_epoch_seconds();
2131
2132 anyhow::ensure!(
2133 (before..=after).contains(&failure.timestamp),
2134 "failure timestamp should use the current epoch"
2135 );
2136 anyhow::ensure!(
2137 FetchFailure::from_bytes(failure.to_bytes()?)? == failure,
2138 "fetch failures should round-trip through JSON"
2139 );
2140 Ok(())
2141 }
2142
2143 #[test]
2144 fn failure_tracking_counts_clears_and_purges() -> Result<()> {
2145 let hash = Hash::from_bytes([22_u8; 32]);
2146 let other_hash = Hash::from_bytes([23_u8; 32]);
2147 let provider = SecretKey::from_bytes(&[22_u8; 32]).public();
2148 let other_provider = SecretKey::from_bytes(&[23_u8; 32]).public();
2149 let mut tracker = ProviderLeaseTracker::with_max_failures_per_provider(2);
2150
2151 for timestamp in 1..=3 {
2152 tracker.record_failure_at(
2153 hash,
2154 provider,
2155 FetchFailure::new_at(FetchFailureKind::NotFound, provider, hash, timestamp, "missing"),
2156 timestamp,
2157 );
2158 }
2159 tracker.record_failure_at(
2160 other_hash,
2161 provider,
2162 FetchFailure::new_at(FetchFailureKind::NotFound, provider, other_hash, 4, "missing"),
2163 4,
2164 );
2165 tracker.record_failure_at(
2166 hash,
2167 other_provider,
2168 FetchFailure::new_at(FetchFailureKind::NotFound, other_provider, hash, 4, "missing"),
2169 4,
2170 );
2171
2172 let record = tracker.failure_record(&hash, &provider).expect("failure record");
2173 anyhow::ensure!(record.failures.len() == 2, "failure details should be capped");
2174 anyhow::ensure!(
2175 record.failures.first().is_some_and(|failure| failure.timestamp == 2),
2176 "oldest detail should be evicted"
2177 );
2178 anyhow::ensure!(
2179 tracker.failure_count(&hash, &provider) == 3,
2180 "aggregate count should be retained"
2181 );
2182 anyhow::ensure!(tracker.consecutive_failures(&hash, &provider) == 3);
2183 anyhow::ensure!(tracker.is_definitively_failed(&hash, &provider));
2184 anyhow::ensure!(!tracker.is_definitively_failed(&other_hash, &provider));
2185 anyhow::ensure!(!tracker.is_definitively_failed(&hash, &other_provider));
2186
2187 tracker.clear_failures_for_provider(&hash, &provider);
2188 anyhow::ensure!(tracker.failure_record(&hash, &provider).is_none());
2189 anyhow::ensure!(tracker.failure_count(&hash, &provider) == 0);
2190
2191 tracker.purge_stale_failures(10, Duration::from_secs(1));
2192 anyhow::ensure!(tracker.failure_record(&other_hash, &provider).is_none());
2193 anyhow::ensure!(tracker.failure_record(&hash, &other_provider).is_none());
2194 Ok(())
2195 }
2196
2197 #[test]
2198 fn transient_failures_do_not_become_definitive() {
2199 let hash = Hash::from_bytes([24_u8; 32]);
2200 let provider = SecretKey::from_bytes(&[24_u8; 32]).public();
2201 let mut tracker = ProviderLeaseTracker::default();
2202 for timestamp in 1..=3 {
2203 tracker.record_failure_at(
2204 hash,
2205 provider,
2206 FetchFailure::new_at(FetchFailureKind::Timeout, provider, hash, timestamp, "timeout"),
2207 timestamp,
2208 );
2209 }
2210 assert_eq!(tracker.consecutive_failures(&hash, &provider), 3);
2211 assert!(!tracker.is_definitively_failed(&hash, &provider));
2212 }
2213
2214 #[test]
2215 fn bans_invalidate_health_and_expire() -> Result<()> {
2216 let hash = Hash::from_bytes([25_u8; 32]);
2217 let (lease, provider_key) = signed_lease(25, hash, 1)?;
2218 let provider = provider_key.public();
2219 let mut tracker = ProviderLeaseTracker::default();
2220 tracker.track_at(lease, 10)?;
2221
2222 tracker.ban_provider(
2223 provider,
2224 None,
2225 "manual test ban",
2226 BanSource::Manual,
2227 Some(Duration::from_secs(10)),
2228 10,
2229 );
2230 anyhow::ensure!(tracker.is_banned(provider, &hash, 19));
2231 anyhow::ensure!(!tracker.is_banned(provider, &hash, 20));
2232 anyhow::ensure!(tracker.health_at(&hash, 19, Duration::from_secs(30)).verified == 0);
2233 anyhow::ensure!(tracker.health_at(&hash, 20, Duration::from_secs(30)).verified == 1);
2234 anyhow::ensure!(tracker.unban_provider(provider, None));
2235 anyhow::ensure!(!tracker.is_banned(provider, &hash, 20));
2236 Ok(())
2237 }
2238
2239 #[test]
2240 fn invalidation_records_scoped_automated_ban() -> Result<()> {
2241 let hash = Hash::from_bytes([26_u8; 32]);
2242 let other_hash = Hash::from_bytes([27_u8; 32]);
2243 let (lease, provider_key) = signed_lease(26, hash, 1)?;
2244 let provider = provider_key.public();
2245 let mut tracker = ProviderLeaseTracker::default();
2246 tracker.track_at(lease, 10)?;
2247
2248 anyhow::ensure!(tracker.invalidate_lease_at(hash, provider, 10));
2249 anyhow::ensure!(tracker.leases_at(&hash, 10).is_empty());
2250 anyhow::ensure!(tracker.is_banned(provider, &hash, 10));
2251 anyhow::ensure!(!tracker.is_banned(provider, &other_hash, 10));
2252 anyhow::ensure!(tracker.banned_providers(&hash, 10) == vec![provider]);
2253 Ok(())
2254 }
2255
2256 #[test]
2257 fn retroactive_invalidation_preserves_success_and_transient_failures() -> Result<()> {
2258 let hash = Hash::from_bytes([28_u8; 32]);
2259 let (failed_lease, failed_key) = signed_lease(28, hash, 1)?;
2260 let (successful_lease, successful_key) = signed_lease(29, hash, 1)?;
2261 let (transient_lease, transient_key) = signed_lease(30, hash, 1)?;
2262 let mut tracker = ProviderLeaseTracker::default();
2263 tracker.track_at(failed_lease, 10)?;
2264 tracker.track_at(successful_lease, 10)?;
2265 tracker.track_at(transient_lease, 10)?;
2266 tracker.record_failure_at(
2267 hash,
2268 failed_key.public(),
2269 FetchFailure::new_at(FetchFailureKind::NotFound, failed_key.public(), hash, 10, "missing"),
2270 10,
2271 );
2272 tracker.record_failure_at(
2273 hash,
2274 transient_key.public(),
2275 FetchFailure::new_at(FetchFailureKind::Timeout, transient_key.public(), hash, 10, "timeout"),
2276 10,
2277 );
2278
2279 let invalidated = tracker.retroactive_invalidate(hash, successful_key.public(), 10);
2280 anyhow::ensure!(invalidated == vec![failed_key.public()]);
2281 anyhow::ensure!(tracker.health_at(&hash, 10, Duration::from_secs(30)).verified == 2);
2282 anyhow::ensure!(
2283 tracker
2284 .leases_at(&hash, 10)
2285 .iter()
2286 .any(|lease| lease.provider == successful_key.public())
2287 );
2288 anyhow::ensure!(
2289 tracker
2290 .leases_at(&hash, 10)
2291 .iter()
2292 .any(|lease| lease.provider == transient_key.public())
2293 );
2294 Ok(())
2295 }
2296
2297 #[tokio::test]
2298 async fn bounded_fetch_validation_rejects_oversized_truncated_and_corrupt_streams() -> Result<()> {
2299 let data = b"bounded fetch";
2300 let hash = Hash::new(data);
2301 let expected_size = u64::try_from(data.len())?;
2302 validate_bounded_fetch(Cursor::new(data), expected_size, hash).await?;
2303 anyhow::ensure!(
2304 validate_bounded_fetch(Cursor::new(b"bounded fetch!"), expected_size, hash)
2305 .await
2306 .is_err(),
2307 "oversized streams should be rejected"
2308 );
2309 anyhow::ensure!(
2310 validate_bounded_fetch(Cursor::new(b"bounded"), expected_size, hash)
2311 .await
2312 .is_err(),
2313 "truncated streams should be rejected"
2314 );
2315 anyhow::ensure!(
2316 validate_bounded_fetch(Cursor::new(b"wrong fetch"), expected_size, hash)
2317 .await
2318 .is_err(),
2319 "hash mismatches should be rejected"
2320 );
2321 Ok(())
2322 }
2323}