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