Skip to main content

syncweb_core/indexing/
reputation.rs

1//! Historical provider reputation and signed trust observations.
2//!
3//! Provider leases describe what a node claims to serve now. Reputation is
4//! deliberately separate: it records what this node has observed over time
5//! and can be combined with the local Web-of-Trust policy at decision time.
6
7use 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::{
22    IndexingDatabase,
23    resilience::FetchFailureKind,
24    wot::{ProviderTrustAction, ProviderTrustRecord, TrustPolicy},
25};
26use crate::{
27    error::{Result, SyncwebError},
28    gossip::SignedGossipMessage,
29    node::gossip_service::GossipService,
30};
31
32const REPUTATION_SIGNAL_CONTEXT: &[u8] = b"syncweb/provider-trust/v1\0";
33const TRUST_STREAM_TOPIC_SEED: &[u8] = b"syncweb/provider-trust-stream/v1";
34const DEFAULT_DECAY_HALF_LIFE: Duration = Duration::from_hours(24);
35const DEFAULT_TEMPORARY_BAN: Duration = Duration::from_hours(1);
36const DEFAULT_BACKOFF_FACTOR: f64 = 2.0;
37const DEFAULT_MAX_BAN: Duration = Duration::from_hours(24 * 30);
38const DEFAULT_SIGNAL_TTL: Duration = Duration::from_hours(168);
39const DEFAULT_SIGNAL_BATCH_SIZE: usize = 64;
40const AUTO_BAN_FAILURE_THRESHOLD: u32 = 3;
41const DEFAULT_BAN_HISTORY_RETENTION: Duration = Duration::from_hours(720);
42
43/// Historical fetch outcomes for one provider.
44#[derive(Clone, Debug, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct ProviderReputation {
47    pub provider: PublicKey,
48    pub total_fetches: u64,
49    pub successful_fetches: u64,
50    pub failed_fetches: u64,
51    pub consecutive_failures: u32,
52    pub last_success_at: Option<u64>,
53    pub last_failure_at: Option<u64>,
54}
55
56impl ProviderReputation {
57    #[must_use]
58    pub const fn new(provider: PublicKey) -> Self {
59        Self {
60            provider,
61            total_fetches: 0,
62            successful_fetches: 0,
63            failed_fetches: 0,
64            consecutive_failures: 0,
65            last_success_at: None,
66            last_failure_at: None,
67        }
68    }
69
70    pub const fn record_success(&mut self, now: u64) {
71        self.total_fetches = self.total_fetches.saturating_add(1);
72        self.successful_fetches = self.successful_fetches.saturating_add(1);
73        self.consecutive_failures = 0;
74        self.last_success_at = Some(now);
75    }
76
77    pub const fn record_failure(&mut self, _kind: FetchFailureKind, now: u64) {
78        self.total_fetches = self.total_fetches.saturating_add(1);
79        self.failed_fetches = self.failed_fetches.saturating_add(1);
80        self.consecutive_failures = self.consecutive_failures.saturating_add(1);
81        self.last_failure_at = Some(now);
82    }
83
84    /// Return a reliability score in `[0.0, 1.0]`.
85    ///
86    /// The standalone score uses the default 24-hour half-life. Stores apply
87    /// their configured half-life and failure weight through the same formula.
88    #[must_use]
89    pub fn reliability_score(&self, now: u64) -> f64 {
90        self.reliability_score_with(now, DEFAULT_DECAY_HALF_LIFE, 2.0)
91    }
92
93    #[must_use]
94    pub fn is_reliable(&self, threshold: f64) -> bool {
95        self.reliability_score(current_epoch_seconds()) > threshold
96    }
97
98    #[must_use]
99    pub fn is_reliable_at(&self, threshold: f64, now: u64) -> bool {
100        self.reliability_score(now) > threshold
101    }
102
103    #[must_use]
104    pub const fn should_auto_ban(&self, consecutive_threshold: u32) -> bool {
105        self.consecutive_failures >= consecutive_threshold
106    }
107
108    fn reliability_score_with(&self, now: u64, half_life: Duration, failure_weight: f64) -> f64 {
109        if self.total_fetches == 0 {
110            return 0.5;
111        }
112        let failure_count = u32::try_from(self.failed_fetches.min(u64::from(u32::MAX))).unwrap_or(u32::MAX);
113        let success_count = u32::try_from(self.successful_fetches.min(u64::from(u32::MAX))).unwrap_or(u32::MAX);
114        let failures = std::ops::Mul::mul(failure_weight.max(0.0), f64::from(failure_count));
115        let denominator = std::ops::Add::add(f64::from(success_count), failures);
116        let base = if denominator == 0.0 {
117            0.5
118        } else {
119            std::ops::Div::div(f64::from(success_count), denominator)
120        };
121        let latest = self.last_success_at.max(self.last_failure_at).unwrap_or(now);
122        let age_seconds = now.saturating_sub(latest).min(u64::from(u32::MAX));
123        let age = f64::from(u32::try_from(age_seconds).unwrap_or(u32::MAX));
124        let half_life_seconds = half_life.as_secs_f64().max(1.0);
125        let decay = 0.5_f64.powf(std::ops::Div::div(age, half_life_seconds));
126        std::ops::Sub::sub(base, 0.5).mul_add(decay, 0.5).clamp(0.0, 1.0)
127    }
128}
129
130/// Configuration for historical reputation scoring.
131#[derive(Clone, Copy, Debug)]
132#[non_exhaustive]
133pub struct ReputationConfig {
134    pub min_samples: usize,
135    pub decay_half_life: Duration,
136    pub failure_weight: f64,
137    pub temporary_ban_duration: Duration,
138    pub auto_ban_backoff_factor: f64,
139    pub max_auto_ban_duration: Duration,
140    pub ban_history_retention: Duration,
141}
142
143impl Default for ReputationConfig {
144    fn default() -> Self {
145        Self {
146            min_samples: 5,
147            decay_half_life: DEFAULT_DECAY_HALF_LIFE,
148            failure_weight: 2.0,
149            temporary_ban_duration: DEFAULT_TEMPORARY_BAN,
150            auto_ban_backoff_factor: DEFAULT_BACKOFF_FACTOR,
151            max_auto_ban_duration: DEFAULT_MAX_BAN,
152            ban_history_retention: DEFAULT_BAN_HISTORY_RETENTION,
153        }
154    }
155}
156
157#[derive(Clone, Debug)]
158struct AutoBan {
159    until: u64,
160    count: u32,
161    last_banned_at: u64,
162}
163
164/// Local provider reputation database.
165#[derive(Clone, Debug)]
166pub struct ProviderReputationStore {
167    reputations: HashMap<PublicKey, ProviderReputation>,
168    config: ReputationConfig,
169    policy: TrustPolicy,
170    auto_bans: HashMap<PublicKey, AutoBan>,
171    signal_sequences: HashMap<(PublicKey, PublicKey), u64>,
172    pending_signals: Vec<ProviderTrustSignal>,
173    max_signal_batch: usize,
174    reporter: Option<PublicKey>,
175    next_signal_sequence: u64,
176    database: Option<IndexingDatabase>,
177}
178
179impl Default for ProviderReputationStore {
180    fn default() -> Self {
181        Self::new(ReputationConfig::default())
182    }
183}
184
185impl ProviderReputationStore {
186    #[must_use]
187    pub fn new(config: ReputationConfig) -> Self {
188        Self {
189            reputations: HashMap::new(),
190            config,
191            policy: TrustPolicy::new(),
192            auto_bans: HashMap::new(),
193            signal_sequences: HashMap::new(),
194            pending_signals: Vec::new(),
195            max_signal_batch: DEFAULT_SIGNAL_BATCH_SIZE,
196            reporter: None,
197            next_signal_sequence: 1,
198            database: None,
199        }
200    }
201
202    /// Create a store backed by an indexing database.
203    ///
204    /// Reputations and signal sequences are hydrated from the database on
205    /// construction. Mutations are persisted through the database.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if the database cannot be read.
210    pub fn with_database(database: IndexingDatabase, config: ReputationConfig) -> Result<Self> {
211        let reputations = database.load_all_reputations().unwrap_or_default();
212        let signal_sequences = database.load_signal_sequences().unwrap_or_default();
213        let auto_bans = Self::reconstruct_auto_bans(&database);
214        Ok(Self {
215            reputations,
216            config,
217            policy: TrustPolicy::new(),
218            auto_bans,
219            signal_sequences,
220            pending_signals: Vec::new(),
221            max_signal_batch: DEFAULT_SIGNAL_BATCH_SIZE,
222            reporter: None,
223            next_signal_sequence: 1,
224            database: Some(database),
225        })
226    }
227
228    fn reconstruct_auto_bans(database: &IndexingDatabase) -> HashMap<PublicKey, AutoBan> {
229        let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
230        database
231            .load_auto_bans()
232            .unwrap_or_default()
233            .into_iter()
234            .filter_map(|(provider, (until, count))| {
235                until.filter(|u| *u > now).map(|u| {
236                    (
237                        provider,
238                        AutoBan {
239                            until: u,
240                            count,
241                            last_banned_at: u,
242                        },
243                    )
244                })
245            })
246            .collect()
247    }
248
249    #[must_use]
250    pub fn with_policy(config: ReputationConfig, policy: TrustPolicy) -> Self {
251        let mut store = Self::new(config);
252        store.policy = policy;
253        store
254    }
255
256    #[must_use]
257    pub const fn config(&self) -> &ReputationConfig {
258        &self.config
259    }
260
261    pub fn set_policy(&mut self, policy: TrustPolicy) {
262        self.policy = policy;
263    }
264
265    /// Configure the reporter identity used for reputation transitions.
266    pub const fn set_reporter(&mut self, reporter: PublicKey) {
267        self.reporter = Some(reporter);
268    }
269
270    /// # Errors
271    ///
272    /// Returns an error when the reporter identity is not a valid Ed25519 key.
273    pub fn trust_reporter(&mut self, reporter: &PublicKey) -> Result<()> {
274        let verifying_key = VerifyingKey::from_bytes(reporter.as_bytes())
275            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid trust reporter: {error}")))?;
276        self.policy.trust_author(&verifying_key);
277        Ok(())
278    }
279
280    #[must_use]
281    pub fn reputation(&self, provider: PublicKey) -> ProviderReputation {
282        self.reputations
283            .get(&provider)
284            .cloned()
285            .unwrap_or_else(|| ProviderReputation::new(provider))
286    }
287
288    #[must_use]
289    pub fn score(&self, provider: PublicKey, now: u64) -> f64 {
290        let reputation = self.reputation(provider);
291        if usize::try_from(reputation.total_fetches).unwrap_or(usize::MAX) < self.config.min_samples {
292            0.5
293        } else {
294            reputation.reliability_score_with(now, self.config.decay_half_life, self.config.failure_weight)
295        }
296    }
297
298    pub fn record_fetch_result(&mut self, provider: PublicKey, success: bool, kind: FetchFailureKind, now: u64) {
299        let previous_score = self.score(provider, now);
300        {
301            let reputation = self
302                .reputations
303                .entry(provider)
304                .or_insert_with(|| ProviderReputation::new(provider));
305            if success {
306                reputation.record_success(now);
307            } else {
308                reputation.record_failure(kind, now);
309                if reputation.should_auto_ban(AUTO_BAN_FAILURE_THRESHOLD)
310                    && self.auto_bans.get(&provider).is_none_or(|ban| ban.until <= now)
311                {
312                    self.apply_auto_ban(provider, now);
313                }
314            }
315        }
316        let current_score = self.score(provider, now);
317        let crossed_to_unreliable = previous_score >= 0.5 && current_score < 0.5;
318        let crossed_to_reliable = previous_score < 0.5 && current_score >= 0.5;
319        if (crossed_to_unreliable || crossed_to_reliable)
320            && let Some(reporter) = self.reporter
321        {
322            let signal_kind = if crossed_to_reliable {
323                TrustSignalKind::ObservedSuccess
324            } else {
325                TrustSignalKind::ObservedFailure
326            };
327            let signal = ProviderTrustSignal {
328                provider,
329                signal: signal_kind,
330                hash: None,
331                reporter,
332                timestamp: now,
333                sequence: self.next_signal_sequence,
334                signature: None,
335            };
336            self.next_signal_sequence = self.next_signal_sequence.saturating_add(1);
337            self.enqueue_signal(signal);
338        }
339        if let Some(ref database) = self.database {
340            let auto_ban_until = self.auto_bans.get(&provider).map(|ban| ban.until);
341            let auto_ban_count = self.auto_bans.get(&provider).map_or(0, |ban| ban.count);
342            if let Some(rep) = self.reputations.get(&provider) {
343                let _ = database.upsert_reputation(&provider, rep, auto_ban_until, auto_ban_count);
344            }
345        }
346    }
347
348    pub fn record_success(&mut self, provider: PublicKey, now: u64) {
349        self.record_fetch_result(provider, true, FetchFailureKind::Unknown, now);
350    }
351
352    pub fn record_failure(&mut self, provider: PublicKey, kind: FetchFailureKind, now: u64) {
353        self.record_fetch_result(provider, false, kind, now);
354    }
355
356    #[must_use]
357    pub fn is_banned(&self, provider: PublicKey, now: u64) -> bool {
358        self.auto_bans.get(&provider).is_some_and(|ban| ban.until > now)
359    }
360
361    #[must_use]
362    pub fn auto_ban_until(&self, provider: PublicKey) -> Option<u64> {
363        self.auto_bans.get(&provider).map(|ban| ban.until)
364    }
365
366    #[must_use]
367    pub fn auto_ban_count(&self, provider: PublicKey) -> u32 {
368        self.auto_bans.get(&provider).map_or(0, |ban| ban.count)
369    }
370
371    /// Clear the active timeout while retaining its backoff history.
372    pub fn rejoin(&mut self, provider: PublicKey, now: u64) -> bool {
373        self.auto_bans.get(&provider).is_some_and(|ban| ban.until <= now)
374    }
375
376    #[must_use]
377    pub fn should_skip_provider(&self, provider: PublicKey, now: u64, threshold: f64) -> bool {
378        self.is_banned(provider, now) || self.score(provider, now) < threshold
379    }
380
381    /// Return all known providers, ranked by score and then hash distance.
382    #[must_use]
383    pub fn rank_providers(&self, now: u64, hash: Hash) -> Vec<PublicKey> {
384        let providers = self.reputations.keys().copied().collect::<Vec<_>>();
385        self.rank_provider_list(now, hash, &providers)
386    }
387
388    #[must_use]
389    pub fn rank_provider_list(&self, now: u64, hash: Hash, providers: &[PublicKey]) -> Vec<PublicKey> {
390        let mut unique = HashSet::new();
391        let mut ranked = providers
392            .iter()
393            .copied()
394            .filter(|provider| unique.insert(*provider))
395            .map(|provider| (self.score(provider, now), provider))
396            .collect::<Vec<_>>();
397        ranked.sort_by(|left, right| {
398            right
399                .0
400                .total_cmp(&left.0)
401                .then_with(|| xor_distance(hash, right.1).cmp(&xor_distance(hash, left.1)))
402                .then_with(|| left.1.as_bytes().cmp(right.1.as_bytes()))
403        });
404        ranked.into_iter().map(|(_, provider)| provider).collect()
405    }
406
407    pub fn purge_stale(&mut self, now: u64, ttl: Duration) {
408        let ttl_seconds = ttl.as_secs();
409        self.reputations.retain(|_, reputation| {
410            let last = reputation.last_success_at.max(reputation.last_failure_at);
411            last.is_some_and(|timestamp| now.saturating_sub(timestamp) <= ttl_seconds)
412        });
413        let retention_secs = self.config.ban_history_retention.as_secs();
414        self.auto_bans
415            .retain(|_, ban| ban.until > now || now.saturating_sub(ban.last_banned_at) <= retention_secs);
416        if let Some(ref database) = self.database {
417            let _ = database.delete_stale_reputations(now, ttl_seconds);
418        }
419    }
420
421    /// Queue one observation, coalescing identical observations in the batch.
422    /// # Errors
423    ///
424    /// Returns an error when the signal structure is invalid.
425    pub fn queue_trust_signal(&mut self, signal: ProviderTrustSignal) -> Result<bool> {
426        signal.validate()?;
427        Ok(self.enqueue_signal(signal))
428    }
429
430    fn enqueue_signal(&mut self, signal: ProviderTrustSignal) -> bool {
431        if self.pending_signals.iter().any(|existing| {
432            existing.provider == signal.provider
433                && existing.reporter == signal.reporter
434                && existing.signal == signal.signal
435                && existing.hash == signal.hash
436        }) {
437            return false;
438        }
439        if self.pending_signals.len() >= self.max_signal_batch {
440            self.pending_signals.remove(0);
441        }
442        self.pending_signals.push(signal);
443        true
444    }
445
446    #[must_use]
447    pub fn pending_trust_signals(&self) -> &[ProviderTrustSignal] {
448        &self.pending_signals
449    }
450
451    pub fn take_pending_trust_signals(&mut self) -> Vec<ProviderTrustSignal> {
452        std::mem::take(&mut self.pending_signals)
453    }
454
455    pub fn set_max_signal_batch(&mut self, max_signal_batch: usize) {
456        self.max_signal_batch = max_signal_batch.max(1);
457        let keep_from = self.pending_signals.len().saturating_sub(self.max_signal_batch);
458        if keep_from > 0 {
459            self.pending_signals.drain(..keep_from);
460        }
461    }
462
463    /// Accept a signed observation from a locally trusted reporter.
464    /// # Errors
465    ///
466    /// Returns an error when the signal is malformed, expired, or unsigned.
467    pub fn ingest_trust_signal(&mut self, signal: ProviderTrustSignal) -> Result<bool> {
468        signal.verify()?;
469        let ProviderTrustSignal {
470            provider,
471            signal: kind,
472            hash,
473            reporter,
474            timestamp,
475            sequence,
476            signature,
477        } = signal;
478        drop(signature);
479        let reporter_id = hex::encode(reporter.as_bytes());
480        if !self.policy.is_trusted_for_at(&reporter_id, hash.as_ref(), timestamp) {
481            return Ok(false);
482        }
483        let sequence_key = (reporter, provider);
484        if self
485            .signal_sequences
486            .get(&sequence_key)
487            .is_some_and(|last_sequence| *last_sequence >= sequence)
488        {
489            return Ok(false);
490        }
491        self.signal_sequences.insert(sequence_key, sequence);
492        match kind {
493            TrustSignalKind::ObservedSuccess => {
494                self.record_fetch_result(provider, true, FetchFailureKind::Unknown, timestamp);
495            }
496            TrustSignalKind::ObservedFailure => {
497                self.record_fetch_result(provider, false, FetchFailureKind::Unknown, timestamp);
498            }
499            TrustSignalKind::ObservedCorruption => {
500                self.record_fetch_result(provider, false, FetchFailureKind::Corruption, timestamp);
501            }
502        }
503        Ok(true)
504    }
505
506    /// Publish a signed signal to the provider-trust stream.
507    ///
508    /// # Errors
509    ///
510    /// Returns an error when the signal is invalid or gossip rejects the
511    /// publication.
512    pub async fn publish_signal(
513        &self,
514        gossip: &GossipService,
515        sender: &GossipSender,
516        signal: &ProviderTrustSignal,
517    ) -> Result<()> {
518        signal.verify()?;
519        gossip.publish(sender, signal.to_bytes()?).await
520    }
521
522    /// Subscribe to the provider-trust stream.
523    ///
524    /// # Errors
525    ///
526    /// Returns an error when the gossip subscription cannot be created.
527    pub async fn subscribe_trust_stream(
528        &self,
529        gossip: &GossipService,
530        bootstrap: Vec<PublicKey>,
531    ) -> Result<GossipTopic> {
532        gossip.subscribe(trust_stream_topic(), bootstrap).await
533    }
534
535    fn apply_auto_ban(&mut self, provider: PublicKey, now: u64) {
536        let previous_count = self.auto_bans.get(&provider).map_or(0, |ban| ban.count);
537        let count = previous_count.saturating_add(1);
538        let exponent = i32::try_from(count.saturating_sub(1)).unwrap_or(i32::MAX);
539        let scaled = std::ops::Mul::mul(
540            self.config.temporary_ban_duration.as_secs_f64(),
541            self.config.auto_ban_backoff_factor.max(1.0).powi(exponent),
542        );
543        let duration = Duration::from_secs_f64(scaled.min(self.config.max_auto_ban_duration.as_secs_f64()));
544        self.auto_bans.insert(
545            provider,
546            AutoBan {
547                until: now.saturating_add(duration.as_secs()),
548                count,
549                last_banned_at: now,
550            },
551        );
552    }
553}
554
555/// A fetch or integrity observation published by a trusted reporter.
556#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
557#[non_exhaustive]
558pub struct ProviderTrustSignal {
559    pub provider: PublicKey,
560    pub signal: TrustSignalKind,
561    pub hash: Option<Hash>,
562    pub reporter: PublicKey,
563    pub timestamp: u64,
564    pub sequence: u64,
565    pub signature: Option<String>,
566}
567
568/// Kinds of automated provider observations.
569#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
570#[non_exhaustive]
571pub enum TrustSignalKind {
572    ObservedSuccess,
573    ObservedFailure,
574    ObservedCorruption,
575}
576
577impl ProviderTrustSignal {
578    /// Create an unsigned signal and validate its structure.
579    ///
580    /// # Errors
581    ///
582    /// Returns an error when the signal structure is invalid.
583    pub fn new(
584        provider: PublicKey,
585        kind: TrustSignalKind,
586        hash: Option<Hash>,
587        reporter: PublicKey,
588        timestamp: u64,
589        sequence: u64,
590    ) -> Result<Self> {
591        let signal = Self {
592            provider,
593            signal: kind,
594            hash,
595            reporter,
596            timestamp,
597            sequence,
598            signature: None,
599        };
600        signal.validate()?;
601        Ok(signal)
602    }
603
604    /// Create and sign a signal using the current time.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error when the signal structure is invalid or signing fails.
609    pub fn new_with_time(
610        provider: PublicKey,
611        kind: TrustSignalKind,
612        hash: Option<Hash>,
613        sequence: u64,
614        signing_key: &SigningKey,
615    ) -> Result<Self> {
616        let reporter = PublicKey::from_bytes(&signing_key.verifying_key().to_bytes())
617            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid trust reporter: {error}")))?;
618        let mut created = Self::new(provider, kind, hash, reporter, current_epoch_seconds(), sequence)?;
619        created.sign(signing_key)?;
620        Ok(created)
621    }
622
623    /// Create a signed trust signal from a local provider trust record.
624    ///
625    /// Maps `ProviderTrustAction::Vouch` to `ObservedSuccess` (positive
626    /// observation) and `ProviderTrustAction::Distrust` to `ObservedFailure`
627    /// (negative observation). Only vouch and distrust records can be
628    /// converted; `Trust`, `Warn`, and other actions return an error.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error when the action is not vouch/distrust or when signing
633    /// fails.
634    pub fn from_trust_record(record: &ProviderTrustRecord, signing_key: &SigningKey) -> Result<Self> {
635        Self::new_with_time(
636            record.provider,
637            match record.action {
638                ProviderTrustAction::Vouch => TrustSignalKind::ObservedSuccess,
639                ProviderTrustAction::Distrust => TrustSignalKind::ObservedFailure,
640                ProviderTrustAction::Trust | ProviderTrustAction::Warn => {
641                    return Err(SyncwebError::InvalidConfig(
642                        "only vouch/distrust records can be converted to trust signals".into(),
643                    ));
644                }
645            },
646            record.scope,
647            record.sequence,
648            signing_key,
649        )
650    }
651
652    /// # Errors
653    ///
654    /// Returns an error when the signing key does not match the reporter.
655    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
656        if self.reporter.as_bytes() != signing_key.verifying_key().as_bytes() {
657            return Err(SyncwebError::InvalidIdentity(
658                "provider trust signal signer does not match reporter".to_owned(),
659            ));
660        }
661        self.signature = Some(hex::encode(signing_key.sign(&self.unsigned_bytes()?).to_bytes()));
662        Ok(())
663    }
664
665    /// # Errors
666    ///
667    /// Returns an error when the signing key does not match the reporter.
668    pub fn sign_with_secret_key(&mut self, secret_key: &SecretKey) -> Result<()> {
669        self.sign(&SigningKey::from_bytes(&secret_key.to_bytes()))
670    }
671
672    /// # Errors
673    ///
674    /// Returns an error when the signal is malformed, expired, or unsigned.
675    pub fn verify(&self) -> Result<()> {
676        self.verify_at(current_epoch_seconds())
677    }
678
679    /// # Errors
680    ///
681    /// Returns an error when the signal is malformed, expired, or unsigned.
682    pub fn verify_at(&self, now: u64) -> Result<()> {
683        const CLOCK_SKEW_TOLERANCE: u64 = 3600;
684        self.validate()?;
685        if self.timestamp > now.saturating_add(CLOCK_SKEW_TOLERANCE) {
686            return Err(SyncwebError::InvalidConfig(
687                "provider trust signal is from the future".to_owned(),
688            ));
689        }
690        if now.saturating_sub(self.timestamp) > DEFAULT_SIGNAL_TTL.as_secs() {
691            return Err(SyncwebError::InvalidConfig(
692                "provider trust signal has expired".to_owned(),
693            ));
694        }
695        let signature_text = self
696            .signature
697            .as_deref()
698            .ok_or_else(|| SyncwebError::InvalidConfig("provider trust signal must contain a signature".to_owned()))?;
699        let signature_bytes = hex::decode(signature_text)
700            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid provider trust signature: {error}")))?;
701        let signature = Signature::from_slice(&signature_bytes)
702            .map_err(|error| SyncwebError::InvalidConfig(format!("invalid provider trust signature: {error}")))?;
703        let key = VerifyingKey::from_bytes(self.reporter.as_bytes())
704            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider trust reporter: {error}")))?;
705        key.verify(&self.unsigned_bytes()?, &signature)
706            .map_err(|error| SyncwebError::InvalidConfig(format!("provider trust signature is invalid: {error}")))
707    }
708
709    /// # Errors
710    ///
711    /// Returns an error when an identity or sequence is invalid.
712    pub fn validate(&self) -> Result<()> {
713        if self.sequence == 0 {
714            return Err(SyncwebError::InvalidConfig(
715                "provider trust signal sequence must be greater than zero".to_owned(),
716            ));
717        }
718        let _provider = VerifyingKey::from_bytes(self.provider.as_bytes())
719            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider identity: {error}")))?;
720        let _reporter = VerifyingKey::from_bytes(self.reporter.as_bytes())
721            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid reporter identity: {error}")))?;
722        Ok(())
723    }
724
725    /// # Errors
726    ///
727    /// Returns an error when the signal is malformed or serialization fails.
728    pub fn to_bytes(&self) -> Result<Vec<u8>> {
729        self.validate()?;
730        serde_json::to_vec(self)
731            .map_err(|error| SyncwebError::operation("failed to serialize provider trust signal", error))
732    }
733
734    /// # Errors
735    ///
736    /// Returns an error when the bytes do not contain a valid signal.
737    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
738        let signal: Self = serde_json::from_slice(bytes.as_ref())
739            .map_err(|error| SyncwebError::operation("failed to deserialize provider trust signal", error))?;
740        signal.validate()?;
741        Ok(signal)
742    }
743
744    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
745        let mut unsigned = self.clone();
746        unsigned.signature = None;
747        let encoded = serde_json::to_vec(&unsigned)
748            .map_err(|error| SyncwebError::operation("failed to serialize unsigned provider trust signal", error))?;
749        let mut bytes = Vec::with_capacity(REPUTATION_SIGNAL_CONTEXT.len().saturating_add(encoded.len()));
750        bytes.extend_from_slice(REPUTATION_SIGNAL_CONTEXT);
751        bytes.extend_from_slice(&encoded);
752        Ok(bytes)
753    }
754}
755
756#[must_use]
757pub fn trust_stream_topic() -> TopicId {
758    TopicId::from_bytes(*blake3::hash(TRUST_STREAM_TOPIC_SEED).as_bytes())
759}
760
761fn xor_distance(hash: Hash, provider: PublicKey) -> [u8; 32] {
762    let mut distance = [0_u8; 32];
763    for ((target, hash_byte), provider_byte) in distance.iter_mut().zip(hash.as_bytes()).zip(provider.as_bytes()) {
764        *target = *hash_byte ^ *provider_byte;
765    }
766    distance
767}
768
769impl SignedGossipMessage for ProviderTrustSignal {
770    fn verify_signature(&self) -> Result<()> {
771        self.verify()
772    }
773}
774
775fn current_epoch_seconds() -> u64 {
776    SystemTime::now()
777        .duration_since(UNIX_EPOCH)
778        .map_or(0, |duration| duration.as_secs())
779}