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}