prns-runtime 0.3.4

Runtime-neutral Personal Reticulum node contracts and manifold kernel
Documentation
use alloc::collections::{BTreeMap, VecDeque};
use alloc::string::String;
use alloc::vec::Vec;
use core::future::Future;

use prns_core::engine::{AnnounceRateState, RouteSnapshot};
use prns_core::interfaces::PacketPhyStats;
use prns_core::routing::announce::AnnounceRateAccounting;
use prns_core::routing::dedup::PacketHash;
use prns_core::units::InstantMillis;
use prns_core::wire::{DestinationHash, TRUNCATED_HASH_BYTE_LEN};

use super::super::{fold_logical_interface_inventory, InterfaceInventoryEntry};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnounceRateSnapshot {
    pub destination: DestinationHash,
    pub last_allowed_announce_at: InstantMillis,
    pub blocked_until: InstantMillis,
    pub rate_violations: u16,
    pub observed_at: Vec<InstantMillis>,
}

const MAX_ANNOUNCE_RATE_OBSERVATIONS: usize = 16;

#[derive(Default)]
pub struct HeapAnnounceRateHistory {
    observed_at: BTreeMap<AnnounceRateHistoryKey, VecDeque<InstantMillis>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct AnnounceRateHistoryKey([u8; TRUNCATED_HASH_BYTE_LEN]);

impl From<DestinationHash> for AnnounceRateHistoryKey {
    fn from(destination: DestinationHash) -> Self {
        Self(*destination.as_bytes())
    }
}

impl HeapAnnounceRateHistory {
    pub fn record(
        &mut self,
        destination: DestinationHash,
        observed_at: InstantMillis,
        accounting: AnnounceRateAccounting,
    ) {
        match accounting {
            AnnounceRateAccounting::NotApplied => return,
            AnnounceRateAccounting::Started => {
                self.observed_at.insert(destination.into(), VecDeque::new());
            }
            AnnounceRateAccounting::Continued => {}
        }
        let history = self.observed_at.entry(destination.into()).or_default();
        if history.len() == MAX_ANNOUNCE_RATE_OBSERVATIONS {
            history.pop_front();
        }
        history.push_back(observed_at);
    }

    #[must_use]
    pub fn snapshot(&self, state: AnnounceRateState) -> AnnounceRateSnapshot {
        AnnounceRateSnapshot {
            destination: state.destination,
            last_allowed_announce_at: state.last_allowed_announce_at,
            blocked_until: state.blocked_until,
            rate_violations: state.rate_violations,
            observed_at: self
                .observed_at
                .get(&state.destination.into())
                .map(|history| history.iter().copied().collect())
                .unwrap_or_default(),
        }
    }
}

pub trait NodeIntrospection {
    fn interface_inventory(&self) -> Vec<InterfaceInventoryEntry<String>>;

    fn link_count(&self) -> impl Future<Output = u32> + Send;

    fn packet_phy(&self, packet_hash: PacketHash) -> Option<PacketPhyStats>;

    fn announce_rates(&self) -> impl Future<Output = Vec<AnnounceRateSnapshot>> + Send;

    fn routes(&self) -> impl Future<Output = Vec<RouteSnapshot>> + Send;

    fn route(
        &self,
        destination: DestinationHash,
    ) -> impl Future<Output = Option<RouteSnapshot>> + Send;
}

#[must_use]
pub fn logical_interface_inventory<Label: Ord>(
    mut inventory: Vec<InterfaceInventoryEntry<Label>>,
) -> Vec<InterfaceInventoryEntry<Label>> {
    let logical_len = fold_logical_interface_inventory(&mut inventory).len();
    inventory.truncate(logical_len);
    inventory
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn announce_rate_history_is_bounded_and_restartable() {
        let destination = DestinationHash::new([0x42; 16]);
        let mut history = HeapAnnounceRateHistory::default();
        history.record(
            destination,
            InstantMillis(99),
            AnnounceRateAccounting::Started,
        );
        for observed_at in 0..20 {
            history.record(
                destination,
                InstantMillis(observed_at),
                AnnounceRateAccounting::Continued,
            );
        }

        let snapshot = history.snapshot(AnnounceRateState {
            destination,
            last_allowed_announce_at: InstantMillis(19),
            blocked_until: InstantMillis(0),
            rate_violations: 0,
        });

        assert_eq!(
            snapshot.observed_at,
            (4..20).map(InstantMillis).collect::<Vec<_>>()
        );

        history.record(
            destination,
            InstantMillis(25),
            AnnounceRateAccounting::Started,
        );

        assert_eq!(
            history
                .snapshot(AnnounceRateState {
                    destination,
                    last_allowed_announce_at: InstantMillis(25),
                    blocked_until: InstantMillis(0),
                    rate_violations: 0,
                })
                .observed_at,
            vec![InstantMillis(25)]
        );
    }
}