Skip to main content

nwep/
cache.rs

1use crate::error::{Error, check};
2use crate::ffi;
3use crate::types::{Duration, NodeId, Tstamp};
4
5/// `CACHE_DEFAULT_CAPACITY` is the default maximum number of entries in the identity cache.
6pub const CACHE_DEFAULT_CAPACITY: usize = 10000;
7
8/// `CACHE_DEFAULT_TTL` is the default time-to-live for a cached identity entry (1 hour in nanoseconds).
9pub const CACHE_DEFAULT_TTL: Duration = 3600 * crate::types::SECONDS;
10
11/// `IdentityCacheSettings` configures the capacity and entry lifetime for an [`IdentityCache`].
12#[derive(Clone, Debug)]
13pub struct IdentityCacheSettings {
14    /// Maximum number of entries before the oldest are evicted.
15    pub capacity: usize,
16    /// Time-to-live for each entry in nanoseconds. Entries older than `ttl_ns` are not returned by [`IdentityCache::lookup`].
17    pub ttl_ns: Duration,
18}
19
20impl Default for IdentityCacheSettings {
21    fn default() -> Self {
22        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_identity_cache_settings>() };
23        unsafe { ffi::nwep_identity_cache_settings_default(&mut s) };
24        IdentityCacheSettings {
25            capacity: s.capacity,
26            ttl_ns: s.ttl_ns,
27        }
28    }
29}
30
31/// `CachedIdentity` is a verified identity record stored in the [`IdentityCache`].
32#[derive(Clone, Debug)]
33pub struct CachedIdentity {
34    /// The node identifier.
35    pub node_id: NodeId,
36    /// The node's active Ed25519 public key at the time of verification.
37    pub pubkey: [u8; 32],
38    /// Merkle log position of the entry that was verified.
39    pub log_index: u64,
40    /// Timestamp (nanoseconds since epoch) when this entry was cached.
41    pub verified_at: Tstamp,
42    /// Timestamp after which this entry is considered expired and must be re-verified.
43    pub expires_at: Tstamp,
44}
45
46impl CachedIdentity {
47    fn from_ffi(c: &ffi::nwep_cached_identity) -> Self {
48        CachedIdentity {
49            node_id: NodeId(c.nodeid.data),
50            pubkey: c.pubkey,
51            log_index: c.log_index,
52            verified_at: c.verified_at,
53            expires_at: c.expires_at,
54        }
55    }
56}
57
58/// `CacheStats` holds counters for monitoring [`IdentityCache`] performance.
59#[derive(Clone, Debug, Default)]
60pub struct CacheStats {
61    /// Number of successful lookups that returned a cached entry.
62    pub hits: u64,
63    /// Number of lookups that found no valid cached entry.
64    pub misses: u64,
65    /// Number of entries evicted due to capacity overflow.
66    pub evictions: u64,
67    /// Number of entries written to the cache via [`IdentityCache::store`].
68    pub stores: u64,
69    /// Number of entries invalidated via [`IdentityCache::invalidate`], [`on_rotation`](IdentityCache::on_rotation), or [`on_revocation`](IdentityCache::on_revocation).
70    pub invalidations: u64,
71}
72
73/// `IdentityCache` is an LRU cache that maps [`NodeId`] to a verified identity record.
74///
75/// `IdentityCache` avoids repeated Merkle proof verification for frequently-seen peers.
76/// Callers must propagate key rotation and revocation events via [`on_rotation`](IdentityCache::on_rotation)
77/// and [`on_revocation`](IdentityCache::on_revocation) to keep cached entries consistent with the log.
78pub struct IdentityCache {
79    ptr: *mut ffi::nwep_identity_cache,
80}
81
82unsafe impl Send for IdentityCache {}
83
84impl IdentityCache {
85    /// `new` creates an identity cache with the given settings.
86    ///
87    /// Pass `None` to use the default settings ([`CACHE_DEFAULT_CAPACITY`], [`CACHE_DEFAULT_TTL`]).
88    ///
89    /// # Errors
90    ///
91    /// Returns `Err` if the underlying C allocation fails.
92    pub fn new(settings: Option<&IdentityCacheSettings>) -> Result<Self, Error> {
93        let mut ptr: *mut ffi::nwep_identity_cache = std::ptr::null_mut();
94        let rc = match settings {
95            Some(s) => {
96                let ffi_s = ffi::nwep_identity_cache_settings {
97                    capacity: s.capacity,
98                    ttl_ns: s.ttl_ns,
99                };
100                unsafe { ffi::nwep_identity_cache_new(&mut ptr, &ffi_s) }
101            }
102            None => unsafe { ffi::nwep_identity_cache_new(&mut ptr, std::ptr::null()) },
103        };
104        check(rc)?;
105        Ok(IdentityCache { ptr })
106    }
107
108    /// `lookup` retrieves the cached identity for a node if it exists and has not expired.
109    ///
110    /// # Errors
111    ///
112    /// Returns `Err` if no valid (non-expired) entry is found for `node_id`.
113    pub fn lookup(&mut self, node_id: &NodeId, now: Tstamp) -> Result<CachedIdentity, Error> {
114        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
115        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_cached_identity>() };
116        check(unsafe { ffi::nwep_identity_cache_lookup(self.ptr, &ffi_nid, now, &mut out) })?;
117        Ok(CachedIdentity::from_ffi(&out))
118    }
119
120    /// `store` adds or updates a verified identity entry in the cache.
121    ///
122    /// `now` is used to set the `verified_at` and `expires_at` fields of the stored entry.
123    ///
124    /// # Errors
125    ///
126    /// Returns `Err` if the C call fails.
127    pub fn store(
128        &mut self,
129        node_id: &NodeId,
130        pubkey: &[u8; 32],
131        log_index: u64,
132        now: Tstamp,
133    ) -> Result<(), Error> {
134        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
135        check(unsafe {
136            ffi::nwep_identity_cache_store(self.ptr, &ffi_nid, pubkey.as_ptr(), log_index, now)
137        })
138    }
139
140    /// `invalidate` removes the cached entry for a node, forcing re-verification on next lookup.
141    ///
142    /// # Errors
143    ///
144    /// Returns `Err` if the C call fails (e.g. node not in cache).
145    pub fn invalidate(&mut self, node_id: &NodeId) -> Result<(), Error> {
146        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
147        check(unsafe { ffi::nwep_identity_cache_invalidate(self.ptr, &ffi_nid) })
148    }
149
150    /// `clear` removes all entries from the cache.
151    pub fn clear(&mut self) {
152        unsafe { ffi::nwep_identity_cache_clear(self.ptr) }
153    }
154
155    /// `size` returns the current number of entries in the cache.
156    pub fn size(&self) -> usize {
157        unsafe { ffi::nwep_identity_cache_size(self.ptr) }
158    }
159
160    /// `capacity` returns the maximum number of entries the cache can hold before eviction.
161    pub fn capacity(&self) -> usize {
162        unsafe { ffi::nwep_identity_cache_capacity(self.ptr) }
163    }
164
165    /// `on_rotation` updates a cached entry after a node rotates its key.
166    ///
167    /// Updates the stored pubkey and log_index so subsequent lookups return the new key
168    /// without requiring a fresh Merkle proof verification.
169    ///
170    /// # Errors
171    ///
172    /// Returns `Err` if the C call fails (e.g. node not in cache).
173    pub fn on_rotation(
174        &mut self,
175        node_id: &NodeId,
176        new_pubkey: &[u8; 32],
177        new_log_index: u64,
178        now: Tstamp,
179    ) -> Result<(), Error> {
180        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
181        check(unsafe {
182            ffi::nwep_identity_cache_on_rotation(
183                self.ptr,
184                &ffi_nid,
185                new_pubkey.as_ptr(),
186                new_log_index,
187                now,
188            )
189        })
190    }
191
192    /// `on_revocation` marks a node as revoked in the cache.
193    ///
194    /// After this call, [`lookup`](IdentityCache::lookup) will return an entry with
195    /// a revoked flag, so callers can reject the peer without a full Merkle verification.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Err` if the C call fails.
200    pub fn on_revocation(&mut self, node_id: &NodeId) -> Result<(), Error> {
201        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
202        check(unsafe { ffi::nwep_identity_cache_on_revocation(self.ptr, &ffi_nid) })
203    }
204
205    /// `stats` returns a snapshot of the cache performance counters.
206    pub fn stats(&self) -> CacheStats {
207        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_cache_stats>() };
208        unsafe { ffi::nwep_identity_cache_get_stats(self.ptr, &mut s) };
209        CacheStats {
210            hits: s.hits,
211            misses: s.misses,
212            evictions: s.evictions,
213            stores: s.stores,
214            invalidations: s.invalidations,
215        }
216    }
217
218    /// `reset_stats` zeroes all performance counters.
219    pub fn reset_stats(&mut self) {
220        unsafe { ffi::nwep_identity_cache_reset_stats(self.ptr) }
221    }
222}
223
224impl Drop for IdentityCache {
225    fn drop(&mut self) {
226        if !self.ptr.is_null() {
227            unsafe { ffi::nwep_identity_cache_free(self.ptr) }
228        }
229    }
230}