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::{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/// Historical fetch outcomes for one provider.
38#[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    /// Return a reliability score in `[0.0, 1.0]`.
79    ///
80    /// The standalone score uses the default 24-hour half-life. Stores apply
81    /// their configured half-life and failure weight through the same formula.
82    #[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/// Configuration for historical reputation scoring.
125#[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/// Local provider reputation database.
156#[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    /// Configure the reporter identity used for reputation transitions.
208    pub const fn set_reporter(&mut self, reporter: PublicKey) {
209        self.reporter = Some(reporter);
210    }
211
212    /// # Errors
213    ///
214    /// Returns an error when the reporter identity is not a valid Ed25519 key.
215    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    /// Clear the active timeout while retaining its backoff history.
303    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    /// Return all known providers, ranked by score and then hash distance.
313    #[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    /// Queue one observation, coalescing identical observations in the batch.
348    /// # Errors
349    ///
350    /// Returns an error when the signal structure is invalid.
351    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    /// Accept a signed observation from a locally trusted reporter.
390    /// # Errors
391    ///
392    /// Returns an error when the signal is malformed, expired, or unsigned.
393    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    /// Publish a signed signal to the provider-trust stream.
433    ///
434    /// # Errors
435    ///
436    /// Returns an error when the signal is invalid or gossip rejects the
437    /// publication.
438    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    /// Subscribe to the provider-trust stream.
449    ///
450    /// # Errors
451    ///
452    /// Returns an error when the gossip subscription cannot be created.
453    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/// A fetch or integrity observation published by a trusted reporter.
481#[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/// Kinds of automated provider observations.
494#[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    /// Create an unsigned signal and validate its structure.
504    ///
505    /// # Errors
506    ///
507    /// Returns an error when the signal structure is invalid.
508    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    /// Create and sign a signal using the current time.
530    ///
531    /// # Errors
532    ///
533    /// Returns an error when the signal structure is invalid or signing fails.
534    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    /// # Errors
549    ///
550    /// Returns an error when the signing key does not match the reporter.
551    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    /// # Errors
562    ///
563    /// Returns an error when the signing key does not match the reporter.
564    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    /// # Errors
569    ///
570    /// Returns an error when the signal is malformed, expired, or unsigned.
571    pub fn verify(&self) -> Result<()> {
572        self.verify_at(current_epoch_seconds())
573    }
574
575    /// # Errors
576    ///
577    /// Returns an error when the signal is malformed, expired, or unsigned.
578    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    /// # Errors
605    ///
606    /// Returns an error when an identity or sequence is invalid.
607    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    /// # Errors
621    ///
622    /// Returns an error when the signal is malformed or serialization fails.
623    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    /// # Errors
630    ///
631    /// Returns an error when the bytes do not contain a valid signal.
632    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}