Skip to main content

saorsa_rsps/
lib.rs

1// Copyright 2024 Saorsa Labs
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4#![forbid(unsafe_code)]
5
6//! # DHT RSPS - Root-Scoped Provider Summaries
7//!
8//! This crate implements Root-Scoped Provider Summaries using Golomb Coded Sets (GCS)
9//! for efficient DHT lookups and cache management in the P2P network.
10//!
11//! ## Features
12//! - Golomb Coded Sets for space-efficient CID summaries
13//! - Root-anchored cache admission policies
14//! - TTL management with hit and receipt tracking
15//! - Witness receipts with VRF pseudonyms
16
17use std::time::{Duration, SystemTime};
18use thiserror::Error;
19
20pub mod cache;
21pub mod crypto;
22pub mod gcs;
23pub mod ttl;
24pub mod witness;
25
26pub use cache::{CachePolicy, RootAnchoredCache};
27pub use gcs::{GcsBuilder, GolombCodedSet};
28pub use ttl::{TtlConfig, TtlEngine, TtlStats};
29pub use witness::{VrfPseudonym, WitnessKey, WitnessReceipt};
30
31/// Errors that can occur in RSPS operations
32#[derive(Debug, Error)]
33pub enum RspsError {
34    #[error("Invalid parameters: {0}")]
35    InvalidParameters(String),
36
37    #[error("GCS build failed: {0}")]
38    GcsBuildError(String),
39
40    #[error("Cache admission denied: {0}")]
41    CacheAdmissionDenied(String),
42
43    #[error("TTL expired")]
44    TtlExpired,
45
46    #[error("Invalid witness receipt: {0}")]
47    InvalidWitness(String),
48
49    #[error("Cryptographic operation failed: {0}")]
50    CryptoError(String),
51
52    #[error("IO error: {0}")]
53    Io(#[from] std::io::Error),
54}
55
56impl From<crate::crypto::CryptoError> for RspsError {
57    fn from(err: crate::crypto::CryptoError) -> Self {
58        RspsError::CryptoError(err.to_string())
59    }
60}
61
62pub type Result<T> = std::result::Result<T, RspsError>;
63
64/// Content identifier (CID) type
65pub type Cid = [u8; 32];
66
67/// Root identifier
68pub type RootCid = [u8; 32];
69
70/// RSPS configuration
71#[derive(Debug, Clone)]
72pub struct RspsConfig {
73    /// Target false positive rate for GCS
74    pub target_fpr: f64,
75    /// Base TTL for cache entries
76    pub base_ttl: Duration,
77    /// TTL extension per hit
78    pub ttl_per_hit: Duration,
79    /// Maximum TTL from hits
80    pub max_hit_ttl: Duration,
81    /// TTL extension per witness receipt
82    pub ttl_per_receipt: Duration,
83    /// Maximum TTL from receipts
84    pub max_receipt_ttl: Duration,
85    /// Temporal bucketing window for receipts
86    pub receipt_bucket_window: Duration,
87}
88
89impl Default for RspsConfig {
90    fn default() -> Self {
91        Self {
92            target_fpr: 5e-4,                                   // 0.05% false positive rate
93            base_ttl: Duration::from_secs(2 * 3600),            // 2 hours
94            ttl_per_hit: Duration::from_secs(30 * 60),          // 30 minutes
95            max_hit_ttl: Duration::from_secs(12 * 3600),        // 12 hours
96            ttl_per_receipt: Duration::from_secs(10 * 60),      // 10 minutes
97            max_receipt_ttl: Duration::from_secs(2 * 3600),     // 2 hours
98            receipt_bucket_window: Duration::from_secs(5 * 60), // 5 minutes
99        }
100    }
101}
102
103/// Root-Scoped Provider Summary
104#[derive(Debug, Clone)]
105pub struct Rsps {
106    /// The root CID this summary is for
107    pub root_cid: RootCid,
108    /// The epoch this summary represents
109    pub epoch: u64,
110    /// The GCS containing CIDs under this root
111    pub gcs: GolombCodedSet,
112    /// Creation timestamp
113    pub created_at: SystemTime,
114    /// Salt used for GCS
115    pub salt: [u8; 32],
116}
117
118impl Rsps {
119    /// Create a new RSPS for a root with given CIDs
120    pub fn new(root_cid: RootCid, epoch: u64, cids: &[Cid], config: &RspsConfig) -> Result<Self> {
121        // Generate salt from root_cid and epoch
122        let salt = Self::generate_salt(&root_cid, epoch);
123
124        // Build GCS with target FPR
125        let gcs = GcsBuilder::new()
126            .target_fpr(config.target_fpr)
127            .salt(&salt)
128            .build(cids)?;
129
130        Ok(Self {
131            root_cid,
132            epoch,
133            gcs,
134            created_at: SystemTime::now(),
135            salt,
136        })
137    }
138
139    /// Check if a CID might be in this root
140    pub fn contains(&self, cid: &Cid) -> bool {
141        self.gcs.contains(cid)
142    }
143
144    /// Get the digest of this RSPS for DHT advertisement
145    pub fn digest(&self) -> [u8; 32] {
146        use blake3::Hasher;
147        let mut hasher = Hasher::new();
148        hasher.update(&self.root_cid);
149        hasher.update(&self.epoch.to_le_bytes());
150        hasher.update(&self.gcs.to_bytes());
151        let mut digest = [0u8; 32];
152        digest.copy_from_slice(hasher.finalize().as_bytes());
153        digest
154    }
155
156    /// Generate deterministic salt for GCS
157    fn generate_salt(root_cid: &RootCid, epoch: u64) -> [u8; 32] {
158        use blake3::Hasher;
159        let mut hasher = Hasher::new();
160        hasher.update(b"rsps-salt");
161        hasher.update(root_cid);
162        hasher.update(&epoch.to_le_bytes());
163        let mut salt = [0u8; 32];
164        salt.copy_from_slice(hasher.finalize().as_bytes());
165        salt
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_rsps_creation() {
175        let root_cid = [1u8; 32];
176        let cids = vec![[2u8; 32], [3u8; 32], [4u8; 32]];
177        let config = RspsConfig::default();
178
179        let rsps = Rsps::new(root_cid, 1, &cids, &config).unwrap();
180
181        // Should contain all added CIDs
182        for cid in &cids {
183            assert!(rsps.contains(cid));
184        }
185
186        // Should not contain random CID (with high probability)
187        let _random_cid = [99u8; 32];
188        // May have false positives at target_fpr rate
189        // This is probabilistic, so we don't assert false
190    }
191
192    #[test]
193    fn test_deterministic_salt() {
194        let root_cid = [1u8; 32];
195        let epoch = 42;
196
197        let salt1 = Rsps::generate_salt(&root_cid, epoch);
198        let salt2 = Rsps::generate_salt(&root_cid, epoch);
199
200        assert_eq!(salt1, salt2, "Salt should be deterministic");
201
202        let different_epoch_salt = Rsps::generate_salt(&root_cid, epoch + 1);
203        assert_ne!(
204            salt1, different_epoch_salt,
205            "Different epochs should produce different salts"
206        );
207    }
208}