nwep-rs 0.1.8

Rust bindings for the NWEP (WEB/1) protocol library
Documentation
use crate::error::{Error, check};
use crate::ffi;
use crate::types::{Duration, NodeId, Tstamp};

/// `CACHE_DEFAULT_CAPACITY` is the default maximum number of entries in the identity cache.
pub const CACHE_DEFAULT_CAPACITY: usize = 10000;

/// `CACHE_DEFAULT_TTL` is the default time-to-live for a cached identity entry (1 hour in nanoseconds).
pub const CACHE_DEFAULT_TTL: Duration = 3600 * crate::types::SECONDS;

/// `IdentityCacheSettings` configures the capacity and entry lifetime for an [`IdentityCache`].
#[derive(Clone, Debug)]
pub struct IdentityCacheSettings {
    /// Maximum number of entries before the oldest are evicted.
    pub capacity: usize,
    /// Time-to-live for each entry in nanoseconds. Entries older than `ttl_ns` are not returned by [`IdentityCache::lookup`].
    pub ttl_ns: Duration,
}

impl Default for IdentityCacheSettings {
    fn default() -> Self {
        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_identity_cache_settings>() };
        unsafe { ffi::nwep_identity_cache_settings_default(&mut s) };
        IdentityCacheSettings {
            capacity: s.capacity,
            ttl_ns: s.ttl_ns,
        }
    }
}

/// `CachedIdentity` is a verified identity record stored in the [`IdentityCache`].
#[derive(Clone, Debug)]
pub struct CachedIdentity {
    /// The node identifier.
    pub node_id: NodeId,
    /// The node's active Ed25519 public key at the time of verification.
    pub pubkey: [u8; 32],
    /// Merkle log position of the entry that was verified.
    pub log_index: u64,
    /// Timestamp (nanoseconds since epoch) when this entry was cached.
    pub verified_at: Tstamp,
    /// Timestamp after which this entry is considered expired and must be re-verified.
    pub expires_at: Tstamp,
}

impl CachedIdentity {
    fn from_ffi(c: &ffi::nwep_cached_identity) -> Self {
        CachedIdentity {
            node_id: NodeId(c.nodeid.data),
            pubkey: c.pubkey,
            log_index: c.log_index,
            verified_at: c.verified_at,
            expires_at: c.expires_at,
        }
    }
}

/// `CacheStats` holds counters for monitoring [`IdentityCache`] performance.
#[derive(Clone, Debug, Default)]
pub struct CacheStats {
    /// Number of successful lookups that returned a cached entry.
    pub hits: u64,
    /// Number of lookups that found no valid cached entry.
    pub misses: u64,
    /// Number of entries evicted due to capacity overflow.
    pub evictions: u64,
    /// Number of entries written to the cache via [`IdentityCache::store`].
    pub stores: u64,
    /// Number of entries invalidated via [`IdentityCache::invalidate`], [`on_rotation`](IdentityCache::on_rotation), or [`on_revocation`](IdentityCache::on_revocation).
    pub invalidations: u64,
}

/// `IdentityCache` is an LRU cache that maps [`NodeId`] to a verified identity record.
///
/// `IdentityCache` avoids repeated Merkle proof verification for frequently-seen peers.
/// Callers must propagate key rotation and revocation events via [`on_rotation`](IdentityCache::on_rotation)
/// and [`on_revocation`](IdentityCache::on_revocation) to keep cached entries consistent with the log.
pub struct IdentityCache {
    ptr: *mut ffi::nwep_identity_cache,
}

unsafe impl Send for IdentityCache {}

impl IdentityCache {
    /// `new` creates an identity cache with the given settings.
    ///
    /// Pass `None` to use the default settings ([`CACHE_DEFAULT_CAPACITY`], [`CACHE_DEFAULT_TTL`]).
    ///
    /// # Errors
    ///
    /// Returns `Err` if the underlying C allocation fails.
    pub fn new(settings: Option<&IdentityCacheSettings>) -> Result<Self, Error> {
        let mut ptr: *mut ffi::nwep_identity_cache = std::ptr::null_mut();
        let rc = match settings {
            Some(s) => {
                let ffi_s = ffi::nwep_identity_cache_settings {
                    capacity: s.capacity,
                    ttl_ns: s.ttl_ns,
                };
                unsafe { ffi::nwep_identity_cache_new(&mut ptr, &ffi_s) }
            }
            None => unsafe { ffi::nwep_identity_cache_new(&mut ptr, std::ptr::null()) },
        };
        check(rc)?;
        Ok(IdentityCache { ptr })
    }

    /// `lookup` retrieves the cached identity for a node if it exists and has not expired.
    ///
    /// # Errors
    ///
    /// Returns `Err` if no valid (non-expired) entry is found for `node_id`.
    pub fn lookup(&mut self, node_id: &NodeId, now: Tstamp) -> Result<CachedIdentity, Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        let mut out = unsafe { std::mem::zeroed::<ffi::nwep_cached_identity>() };
        check(unsafe { ffi::nwep_identity_cache_lookup(self.ptr, &ffi_nid, now, &mut out) })?;
        Ok(CachedIdentity::from_ffi(&out))
    }

    /// `store` adds or updates a verified identity entry in the cache.
    ///
    /// `now` is used to set the `verified_at` and `expires_at` fields of the stored entry.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the C call fails.
    pub fn store(
        &mut self,
        node_id: &NodeId,
        pubkey: &[u8; 32],
        log_index: u64,
        now: Tstamp,
    ) -> Result<(), Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        check(unsafe {
            ffi::nwep_identity_cache_store(self.ptr, &ffi_nid, pubkey.as_ptr(), log_index, now)
        })
    }

    /// `invalidate` removes the cached entry for a node, forcing re-verification on next lookup.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the C call fails (e.g. node not in cache).
    pub fn invalidate(&mut self, node_id: &NodeId) -> Result<(), Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        check(unsafe { ffi::nwep_identity_cache_invalidate(self.ptr, &ffi_nid) })
    }

    /// `clear` removes all entries from the cache.
    pub fn clear(&mut self) {
        unsafe { ffi::nwep_identity_cache_clear(self.ptr) }
    }

    /// `size` returns the current number of entries in the cache.
    pub fn size(&self) -> usize {
        unsafe { ffi::nwep_identity_cache_size(self.ptr) }
    }

    /// `capacity` returns the maximum number of entries the cache can hold before eviction.
    pub fn capacity(&self) -> usize {
        unsafe { ffi::nwep_identity_cache_capacity(self.ptr) }
    }

    /// `on_rotation` updates a cached entry after a node rotates its key.
    ///
    /// Updates the stored pubkey and log_index so subsequent lookups return the new key
    /// without requiring a fresh Merkle proof verification.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the C call fails (e.g. node not in cache).
    pub fn on_rotation(
        &mut self,
        node_id: &NodeId,
        new_pubkey: &[u8; 32],
        new_log_index: u64,
        now: Tstamp,
    ) -> Result<(), Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        check(unsafe {
            ffi::nwep_identity_cache_on_rotation(
                self.ptr,
                &ffi_nid,
                new_pubkey.as_ptr(),
                new_log_index,
                now,
            )
        })
    }

    /// `on_revocation` marks a node as revoked in the cache.
    ///
    /// After this call, [`lookup`](IdentityCache::lookup) will return an entry with
    /// a revoked flag, so callers can reject the peer without a full Merkle verification.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the C call fails.
    pub fn on_revocation(&mut self, node_id: &NodeId) -> Result<(), Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        check(unsafe { ffi::nwep_identity_cache_on_revocation(self.ptr, &ffi_nid) })
    }

    /// `stats` returns a snapshot of the cache performance counters.
    pub fn stats(&self) -> CacheStats {
        let mut s = unsafe { std::mem::zeroed::<ffi::nwep_cache_stats>() };
        unsafe { ffi::nwep_identity_cache_get_stats(self.ptr, &mut s) };
        CacheStats {
            hits: s.hits,
            misses: s.misses,
            evictions: s.evictions,
            stores: s.stores,
            invalidations: s.invalidations,
        }
    }

    /// `reset_stats` zeroes all performance counters.
    pub fn reset_stats(&mut self) {
        unsafe { ffi::nwep_identity_cache_reset_stats(self.ptr) }
    }
}

impl Drop for IdentityCache {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::nwep_identity_cache_free(self.ptr) }
        }
    }
}