1use std::{
8 collections::{HashMap, HashSet},
9 time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
13use iroh::{PublicKey, SecretKey};
14use iroh_blobs::Hash;
15use iroh_gossip::{
16 TopicId,
17 api::{GossipSender, GossipTopic},
18};
19use serde::{Deserialize, Serialize};
20
21use super::{resilience::FetchFailureKind, wot::TrustPolicy};
22use crate::{
23 error::{Result, SyncwebError},
24 node::gossip_service::GossipService,
25};
26
27const REPUTATION_SIGNAL_CONTEXT: &[u8] = b"syncweb/provider-trust/v1\0";
28const TRUST_STREAM_TOPIC_SEED: &[u8] = b"syncweb/provider-trust-stream/v1";
29const DEFAULT_DECAY_HALF_LIFE: Duration = Duration::from_hours(24);
30const DEFAULT_TEMPORARY_BAN: Duration = Duration::from_hours(1);
31const DEFAULT_BACKOFF_FACTOR: f64 = 2.0;
32const DEFAULT_MAX_BAN: Duration = Duration::from_hours(24 * 30);
33const DEFAULT_SIGNAL_TTL: Duration = Duration::from_hours(168);
34const DEFAULT_SIGNAL_BATCH_SIZE: usize = 64;
35const AUTO_BAN_FAILURE_THRESHOLD: u32 = 3;
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct ProviderReputation {
41 pub provider: PublicKey,
42 pub total_fetches: u64,
43 pub successful_fetches: u64,
44 pub failed_fetches: u64,
45 pub consecutive_failures: u32,
46 pub last_success_at: Option<u64>,
47 pub last_failure_at: Option<u64>,
48}
49
50impl ProviderReputation {
51 #[must_use]
52 pub const fn new(provider: PublicKey) -> Self {
53 Self {
54 provider,
55 total_fetches: 0,
56 successful_fetches: 0,
57 failed_fetches: 0,
58 consecutive_failures: 0,
59 last_success_at: None,
60 last_failure_at: None,
61 }
62 }
63
64 pub const fn record_success(&mut self, now: u64) {
65 self.total_fetches = self.total_fetches.saturating_add(1);
66 self.successful_fetches = self.successful_fetches.saturating_add(1);
67 self.consecutive_failures = 0;
68 self.last_success_at = Some(now);
69 }
70
71 pub const fn record_failure(&mut self, _kind: FetchFailureKind, now: u64) {
72 self.total_fetches = self.total_fetches.saturating_add(1);
73 self.failed_fetches = self.failed_fetches.saturating_add(1);
74 self.consecutive_failures = self.consecutive_failures.saturating_add(1);
75 self.last_failure_at = Some(now);
76 }
77
78 #[must_use]
83 pub fn reliability_score(&self, now: u64) -> f64 {
84 self.reliability_score_with(now, DEFAULT_DECAY_HALF_LIFE, 2.0)
85 }
86
87 #[must_use]
88 pub fn is_reliable(&self, threshold: f64) -> bool {
89 self.reliability_score(current_epoch_seconds()) > threshold
90 }
91
92 #[must_use]
93 pub fn is_reliable_at(&self, threshold: f64, now: u64) -> bool {
94 self.reliability_score(now) > threshold
95 }
96
97 #[must_use]
98 pub const fn should_auto_ban(&self, consecutive_threshold: u32) -> bool {
99 self.consecutive_failures >= consecutive_threshold
100 }
101
102 fn reliability_score_with(&self, now: u64, half_life: Duration, failure_weight: f64) -> f64 {
103 if self.total_fetches == 0 {
104 return 0.5;
105 }
106 let failure_count = u32::try_from(self.failed_fetches.min(u64::from(u32::MAX))).unwrap_or(u32::MAX);
107 let success_count = u32::try_from(self.successful_fetches.min(u64::from(u32::MAX))).unwrap_or(u32::MAX);
108 let failures = std::ops::Mul::mul(failure_weight.max(0.0), f64::from(failure_count));
109 let denominator = std::ops::Add::add(f64::from(success_count), failures);
110 let base = if denominator == 0.0 {
111 0.5
112 } else {
113 std::ops::Div::div(f64::from(success_count), denominator)
114 };
115 let latest = self.last_success_at.max(self.last_failure_at).unwrap_or(now);
116 let age_seconds = now.saturating_sub(latest).min(u64::from(u32::MAX));
117 let age = f64::from(u32::try_from(age_seconds).unwrap_or(u32::MAX));
118 let half_life_seconds = half_life.as_secs_f64().max(1.0);
119 let decay = 0.5_f64.powf(std::ops::Div::div(age, half_life_seconds));
120 std::ops::Sub::sub(base, 0.5).mul_add(decay, 0.5).clamp(0.0, 1.0)
121 }
122}
123
124#[derive(Clone, Copy, Debug)]
126#[non_exhaustive]
127pub struct ReputationConfig {
128 pub min_samples: usize,
129 pub decay_half_life: Duration,
130 pub failure_weight: f64,
131 pub temporary_ban_duration: Duration,
132 pub auto_ban_backoff_factor: f64,
133 pub max_auto_ban_duration: Duration,
134}
135
136impl Default for ReputationConfig {
137 fn default() -> Self {
138 Self {
139 min_samples: 5,
140 decay_half_life: DEFAULT_DECAY_HALF_LIFE,
141 failure_weight: 2.0,
142 temporary_ban_duration: DEFAULT_TEMPORARY_BAN,
143 auto_ban_backoff_factor: DEFAULT_BACKOFF_FACTOR,
144 max_auto_ban_duration: DEFAULT_MAX_BAN,
145 }
146 }
147}
148
149#[derive(Clone, Debug)]
150struct AutoBan {
151 until: u64,
152 count: u32,
153}
154
155#[derive(Clone, Debug)]
157pub struct ProviderReputationStore {
158 reputations: HashMap<PublicKey, ProviderReputation>,
159 config: ReputationConfig,
160 policy: TrustPolicy,
161 auto_bans: HashMap<PublicKey, AutoBan>,
162 signal_sequences: HashMap<(PublicKey, PublicKey), u64>,
163 pending_signals: Vec<ProviderTrustSignal>,
164 max_signal_batch: usize,
165 reporter: Option<PublicKey>,
166 next_signal_sequence: u64,
167}
168
169impl Default for ProviderReputationStore {
170 fn default() -> Self {
171 Self::new(ReputationConfig::default())
172 }
173}
174
175impl ProviderReputationStore {
176 #[must_use]
177 pub fn new(config: ReputationConfig) -> Self {
178 Self {
179 reputations: HashMap::new(),
180 config,
181 policy: TrustPolicy::new(),
182 auto_bans: HashMap::new(),
183 signal_sequences: HashMap::new(),
184 pending_signals: Vec::new(),
185 max_signal_batch: DEFAULT_SIGNAL_BATCH_SIZE,
186 reporter: None,
187 next_signal_sequence: 1,
188 }
189 }
190
191 #[must_use]
192 pub fn with_policy(config: ReputationConfig, policy: TrustPolicy) -> Self {
193 let mut store = Self::new(config);
194 store.policy = policy;
195 store
196 }
197
198 #[must_use]
199 pub const fn config(&self) -> &ReputationConfig {
200 &self.config
201 }
202
203 pub fn set_policy(&mut self, policy: TrustPolicy) {
204 self.policy = policy;
205 }
206
207 pub const fn set_reporter(&mut self, reporter: PublicKey) {
209 self.reporter = Some(reporter);
210 }
211
212 pub fn trust_reporter(&mut self, reporter: &PublicKey) -> Result<()> {
216 let verifying_key = VerifyingKey::from_bytes(reporter.as_bytes())
217 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid trust reporter: {error}")))?;
218 self.policy.trust_author(&verifying_key);
219 Ok(())
220 }
221
222 #[must_use]
223 pub fn reputation(&self, provider: PublicKey) -> ProviderReputation {
224 self.reputations
225 .get(&provider)
226 .cloned()
227 .unwrap_or_else(|| ProviderReputation::new(provider))
228 }
229
230 #[must_use]
231 pub fn score(&self, provider: PublicKey, now: u64) -> f64 {
232 let reputation = self.reputation(provider);
233 if usize::try_from(reputation.total_fetches).unwrap_or(usize::MAX) < self.config.min_samples {
234 0.5
235 } else {
236 reputation.reliability_score_with(now, self.config.decay_half_life, self.config.failure_weight)
237 }
238 }
239
240 pub fn record_fetch_result(&mut self, provider: PublicKey, success: bool, kind: FetchFailureKind, now: u64) {
241 let previous_score = self.score(provider, now);
242 let reputation = self
243 .reputations
244 .entry(provider)
245 .or_insert_with(|| ProviderReputation::new(provider));
246 if success {
247 reputation.record_success(now);
248 } else {
249 reputation.record_failure(kind, now);
250 if reputation.should_auto_ban(AUTO_BAN_FAILURE_THRESHOLD) && !self.is_banned(provider, now) {
251 self.apply_auto_ban(provider, now);
252 }
253 }
254 let current_score = self.score(provider, now);
255 let crossed_to_unreliable = previous_score >= 0.5 && current_score < 0.5;
256 let crossed_to_reliable = previous_score < 0.5 && current_score >= 0.5;
257 if (crossed_to_unreliable || crossed_to_reliable)
258 && let Some(reporter) = self.reporter
259 {
260 let signal_kind = if crossed_to_reliable {
261 TrustSignalKind::ObservedSuccess
262 } else {
263 TrustSignalKind::ObservedFailure
264 };
265 let signal = ProviderTrustSignal {
266 provider,
267 signal: signal_kind,
268 hash: None,
269 reporter,
270 timestamp: now,
271 sequence: self.next_signal_sequence,
272 signature: None,
273 };
274 self.next_signal_sequence = self.next_signal_sequence.saturating_add(1);
275 self.enqueue_signal(signal);
276 }
277 }
278
279 pub fn record_success(&mut self, provider: PublicKey, now: u64) {
280 self.record_fetch_result(provider, true, FetchFailureKind::Unknown, now);
281 }
282
283 pub fn record_failure(&mut self, provider: PublicKey, kind: FetchFailureKind, now: u64) {
284 self.record_fetch_result(provider, false, kind, now);
285 }
286
287 #[must_use]
288 pub fn is_banned(&self, provider: PublicKey, now: u64) -> bool {
289 self.auto_bans.get(&provider).is_some_and(|ban| ban.until > now)
290 }
291
292 #[must_use]
293 pub fn auto_ban_until(&self, provider: PublicKey) -> Option<u64> {
294 self.auto_bans.get(&provider).map(|ban| ban.until)
295 }
296
297 #[must_use]
298 pub fn auto_ban_count(&self, provider: PublicKey) -> u32 {
299 self.auto_bans.get(&provider).map_or(0, |ban| ban.count)
300 }
301
302 pub fn rejoin(&mut self, provider: PublicKey, now: u64) -> bool {
304 self.auto_bans.get(&provider).is_some_and(|ban| ban.until <= now)
305 }
306
307 #[must_use]
308 pub fn should_skip_provider(&self, provider: PublicKey, now: u64, threshold: f64) -> bool {
309 self.is_banned(provider, now) || self.score(provider, now) < threshold
310 }
311
312 #[must_use]
314 pub fn rank_providers(&self, now: u64, hash: Hash) -> Vec<PublicKey> {
315 let providers = self.reputations.keys().copied().collect::<Vec<_>>();
316 self.rank_provider_list(now, hash, &providers)
317 }
318
319 #[must_use]
320 pub fn rank_provider_list(&self, now: u64, hash: Hash, providers: &[PublicKey]) -> Vec<PublicKey> {
321 let mut unique = HashSet::new();
322 let mut ranked = providers
323 .iter()
324 .copied()
325 .filter(|provider| unique.insert(*provider))
326 .map(|provider| (self.score(provider, now), provider))
327 .collect::<Vec<_>>();
328 ranked.sort_by(|left, right| {
329 right
330 .0
331 .total_cmp(&left.0)
332 .then_with(|| xor_distance(hash, right.1).cmp(&xor_distance(hash, left.1)))
333 .then_with(|| left.1.as_bytes().cmp(right.1.as_bytes()))
334 });
335 ranked.into_iter().map(|(_, provider)| provider).collect()
336 }
337
338 pub fn purge_stale(&mut self, now: u64, ttl: Duration) {
339 let ttl_seconds = ttl.as_secs();
340 self.reputations.retain(|_, reputation| {
341 let last = reputation.last_success_at.max(reputation.last_failure_at);
342 last.is_some_and(|timestamp| now.saturating_sub(timestamp) <= ttl_seconds)
343 });
344 self.auto_bans.retain(|_, ban| ban.until > now || ban.count > 0);
345 }
346
347 pub fn queue_trust_signal(&mut self, signal: ProviderTrustSignal) -> Result<bool> {
352 signal.validate()?;
353 Ok(self.enqueue_signal(signal))
354 }
355
356 fn enqueue_signal(&mut self, signal: ProviderTrustSignal) -> bool {
357 if self.pending_signals.iter().any(|existing| {
358 existing.provider == signal.provider
359 && existing.reporter == signal.reporter
360 && existing.signal == signal.signal
361 && existing.hash == signal.hash
362 }) {
363 return false;
364 }
365 if self.pending_signals.len() >= self.max_signal_batch {
366 self.pending_signals.remove(0);
367 }
368 self.pending_signals.push(signal);
369 true
370 }
371
372 #[must_use]
373 pub fn pending_trust_signals(&self) -> &[ProviderTrustSignal] {
374 &self.pending_signals
375 }
376
377 pub fn take_pending_trust_signals(&mut self) -> Vec<ProviderTrustSignal> {
378 std::mem::take(&mut self.pending_signals)
379 }
380
381 pub fn set_max_signal_batch(&mut self, max_signal_batch: usize) {
382 self.max_signal_batch = max_signal_batch.max(1);
383 let keep_from = self.pending_signals.len().saturating_sub(self.max_signal_batch);
384 if keep_from > 0 {
385 self.pending_signals.drain(..keep_from);
386 }
387 }
388
389 pub fn ingest_trust_signal(&mut self, signal: ProviderTrustSignal) -> Result<bool> {
394 signal.verify()?;
395 let ProviderTrustSignal {
396 provider,
397 signal: kind,
398 hash,
399 reporter,
400 timestamp,
401 sequence,
402 signature,
403 } = signal;
404 drop(signature);
405 let reporter_id = hex::encode(reporter.as_bytes());
406 if !self.policy.is_trusted_for_at(&reporter_id, hash.as_ref(), timestamp) {
407 return Ok(false);
408 }
409 let sequence_key = (reporter, provider);
410 if self
411 .signal_sequences
412 .get(&sequence_key)
413 .is_some_and(|last_sequence| *last_sequence >= sequence)
414 {
415 return Ok(false);
416 }
417 self.signal_sequences.insert(sequence_key, sequence);
418 match kind {
419 TrustSignalKind::ObservedSuccess => {
420 self.record_fetch_result(provider, true, FetchFailureKind::Unknown, timestamp);
421 }
422 TrustSignalKind::ObservedFailure => {
423 self.record_fetch_result(provider, false, FetchFailureKind::Unknown, timestamp);
424 }
425 TrustSignalKind::ObservedCorruption => {
426 self.record_fetch_result(provider, false, FetchFailureKind::Corruption, timestamp);
427 }
428 }
429 Ok(true)
430 }
431
432 pub async fn publish_signal(
439 &self,
440 gossip: &GossipService,
441 sender: &GossipSender,
442 signal: &ProviderTrustSignal,
443 ) -> Result<()> {
444 signal.verify()?;
445 gossip.publish(sender, signal.to_bytes()?).await
446 }
447
448 pub async fn subscribe_trust_stream(
454 &self,
455 gossip: &GossipService,
456 bootstrap: Vec<PublicKey>,
457 ) -> Result<GossipTopic> {
458 gossip.subscribe(trust_stream_topic(), bootstrap).await
459 }
460
461 fn apply_auto_ban(&mut self, provider: PublicKey, now: u64) {
462 let previous_count = self.auto_bans.get(&provider).map_or(0, |ban| ban.count);
463 let count = previous_count.saturating_add(1);
464 let exponent = i32::try_from(count.saturating_sub(1)).unwrap_or(i32::MAX);
465 let scaled = std::ops::Mul::mul(
466 self.config.temporary_ban_duration.as_secs_f64(),
467 self.config.auto_ban_backoff_factor.max(1.0).powi(exponent),
468 );
469 let duration = Duration::from_secs_f64(scaled.min(self.config.max_auto_ban_duration.as_secs_f64()));
470 self.auto_bans.insert(
471 provider,
472 AutoBan {
473 until: now.saturating_add(duration.as_secs()),
474 count,
475 },
476 );
477 }
478}
479
480#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
482#[non_exhaustive]
483pub struct ProviderTrustSignal {
484 pub provider: PublicKey,
485 pub signal: TrustSignalKind,
486 pub hash: Option<Hash>,
487 pub reporter: PublicKey,
488 pub timestamp: u64,
489 pub sequence: u64,
490 pub signature: Option<String>,
491}
492
493#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
495#[non_exhaustive]
496pub enum TrustSignalKind {
497 ObservedSuccess,
498 ObservedFailure,
499 ObservedCorruption,
500}
501
502impl ProviderTrustSignal {
503 pub fn new(
509 provider: PublicKey,
510 kind: TrustSignalKind,
511 hash: Option<Hash>,
512 reporter: PublicKey,
513 timestamp: u64,
514 sequence: u64,
515 ) -> Result<Self> {
516 let signal = Self {
517 provider,
518 signal: kind,
519 hash,
520 reporter,
521 timestamp,
522 sequence,
523 signature: None,
524 };
525 signal.validate()?;
526 Ok(signal)
527 }
528
529 pub fn new_with_time(
535 provider: PublicKey,
536 kind: TrustSignalKind,
537 hash: Option<Hash>,
538 sequence: u64,
539 signing_key: &SigningKey,
540 ) -> Result<Self> {
541 let reporter = PublicKey::from_bytes(&signing_key.verifying_key().to_bytes())
542 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid trust reporter: {error}")))?;
543 let mut created = Self::new(provider, kind, hash, reporter, current_epoch_seconds(), sequence)?;
544 created.sign(signing_key)?;
545 Ok(created)
546 }
547
548 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
552 if self.reporter.as_bytes() != signing_key.verifying_key().as_bytes() {
553 return Err(SyncwebError::InvalidIdentity(
554 "provider trust signal signer does not match reporter".to_owned(),
555 ));
556 }
557 self.signature = Some(hex::encode(signing_key.sign(&self.unsigned_bytes()?).to_bytes()));
558 Ok(())
559 }
560
561 pub fn sign_with_secret_key(&mut self, secret_key: &SecretKey) -> Result<()> {
565 self.sign(&SigningKey::from_bytes(&secret_key.to_bytes()))
566 }
567
568 pub fn verify(&self) -> Result<()> {
572 self.verify_at(current_epoch_seconds())
573 }
574
575 pub fn verify_at(&self, now: u64) -> Result<()> {
579 self.validate()?;
580 if self.timestamp > now {
581 return Err(SyncwebError::InvalidConfig(
582 "provider trust signal is from the future".to_owned(),
583 ));
584 }
585 if now.saturating_sub(self.timestamp) > DEFAULT_SIGNAL_TTL.as_secs() {
586 return Err(SyncwebError::InvalidConfig(
587 "provider trust signal has expired".to_owned(),
588 ));
589 }
590 let signature_text = self
591 .signature
592 .as_deref()
593 .ok_or_else(|| SyncwebError::InvalidConfig("provider trust signal must contain a signature".to_owned()))?;
594 let signature_bytes = hex::decode(signature_text)
595 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid provider trust signature: {error}")))?;
596 let signature = Signature::from_slice(&signature_bytes)
597 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid provider trust signature: {error}")))?;
598 let key = VerifyingKey::from_bytes(self.reporter.as_bytes())
599 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider trust reporter: {error}")))?;
600 key.verify(&self.unsigned_bytes()?, &signature)
601 .map_err(|error| SyncwebError::InvalidConfig(format!("provider trust signature is invalid: {error}")))
602 }
603
604 pub fn validate(&self) -> Result<()> {
608 if self.sequence == 0 {
609 return Err(SyncwebError::InvalidConfig(
610 "provider trust signal sequence must be greater than zero".to_owned(),
611 ));
612 }
613 let _provider = VerifyingKey::from_bytes(self.provider.as_bytes())
614 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider identity: {error}")))?;
615 let _reporter = VerifyingKey::from_bytes(self.reporter.as_bytes())
616 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid reporter identity: {error}")))?;
617 Ok(())
618 }
619
620 pub fn to_bytes(&self) -> Result<Vec<u8>> {
624 self.validate()?;
625 serde_json::to_vec(self)
626 .map_err(|error| SyncwebError::operation("failed to serialize provider trust signal", error))
627 }
628
629 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
633 let signal: Self = serde_json::from_slice(bytes.as_ref())
634 .map_err(|error| SyncwebError::operation("failed to deserialize provider trust signal", error))?;
635 signal.validate()?;
636 Ok(signal)
637 }
638
639 fn unsigned_bytes(&self) -> Result<Vec<u8>> {
640 let mut unsigned = self.clone();
641 unsigned.signature = None;
642 let encoded = serde_json::to_vec(&unsigned)
643 .map_err(|error| SyncwebError::operation("failed to serialize unsigned provider trust signal", error))?;
644 let mut bytes = Vec::with_capacity(REPUTATION_SIGNAL_CONTEXT.len().saturating_add(encoded.len()));
645 bytes.extend_from_slice(REPUTATION_SIGNAL_CONTEXT);
646 bytes.extend_from_slice(&encoded);
647 Ok(bytes)
648 }
649}
650
651#[must_use]
652pub fn trust_stream_topic() -> TopicId {
653 TopicId::from_bytes(*blake3::hash(TRUST_STREAM_TOPIC_SEED).as_bytes())
654}
655
656fn xor_distance(hash: Hash, provider: PublicKey) -> [u8; 32] {
657 let mut distance = [0_u8; 32];
658 for ((target, hash_byte), provider_byte) in distance.iter_mut().zip(hash.as_bytes()).zip(provider.as_bytes()) {
659 *target = *hash_byte ^ *provider_byte;
660 }
661 distance
662}
663
664fn current_epoch_seconds() -> u64 {
665 SystemTime::now()
666 .duration_since(UNIX_EPOCH)
667 .map_or(0, |duration| duration.as_secs())
668}