prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use alloc::vec::Vec;

use crate::engine::CommandId;
use crate::engine::InstantMillis;
use crate::identity::IdentitySigningPublicKey;
use crate::routing::dedup::PacketHash;
use crate::routing::delivery::receipts::{
    OutstandingReceipt, ReceiptDeadline, ReceiptKind, ReceiptTable, TrackReceiptError,
};

/// RNS 1.4.2 `Transport.MAX_RECEIPTS`: past this, the wrapper culls the stalest receipt so the new send always proceeds. The culled command receives a typed settlement rather than disappearing silently.
pub const DEFAULT_MAX_OUTSTANDING_RECEIPTS: usize = 1024;

#[derive(Debug, Default)]
pub struct HeapReceiptTable {
    packet_hashes: Vec<PacketHash>,
    command_ids: Vec<CommandId>,
    kinds: Vec<ReceiptKind>,
    signing_keys: Vec<IdentitySigningPublicKey>,
    sent_ats: Vec<InstantMillis>,
    deadlines: Vec<ReceiptDeadline>,
}

impl ReceiptTable for HeapReceiptTable {
    fn capacity(&self) -> usize {
        DEFAULT_MAX_OUTSTANDING_RECEIPTS
    }
    fn len(&self) -> usize {
        self.packet_hashes.len()
    }

    fn packet_hashes(&self) -> &[PacketHash] {
        &self.packet_hashes
    }
    fn command_ids(&self) -> &[CommandId] {
        &self.command_ids
    }
    fn kinds(&self) -> &[ReceiptKind] {
        &self.kinds
    }
    fn signing_keys(&self) -> &[IdentitySigningPublicKey] {
        &self.signing_keys
    }
    fn sent_ats(&self) -> &[InstantMillis] {
        &self.sent_ats
    }
    fn deadlines(&self) -> &[ReceiptDeadline] {
        &self.deadlines
    }
    fn set_deadline(&mut self, index: usize, deadline: ReceiptDeadline) {
        if let Some(slot) = self.deadlines.get_mut(index) {
            *slot = deadline;
        }
    }

    fn push(&mut self, receipt: OutstandingReceipt) -> Result<usize, TrackReceiptError> {
        if self.packet_hashes.len() >= DEFAULT_MAX_OUTSTANDING_RECEIPTS {
            return Err(TrackReceiptError::TableFull);
        }
        let index = self.packet_hashes.len();
        self.packet_hashes.push(receipt.packet_hash);
        self.command_ids.push(receipt.command_id);
        self.kinds.push(receipt.kind);
        self.signing_keys.push(receipt.peer_signing_key);
        self.sent_ats.push(receipt.sent_at);
        self.deadlines
            .push(ReceiptDeadline::Due(receipt.timeout_at));
        Ok(index)
    }

    fn remove(&mut self, index: usize) {
        if index >= self.packet_hashes.len() {
            return;
        }
        self.packet_hashes.remove(index);
        self.command_ids.remove(index);
        self.kinds.remove(index);
        self.signing_keys.remove(index);
        self.sent_ats.remove(index);
        self.deadlines.remove(index);
    }
}

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

    #[test]
    fn grows_to_the_reference_cap_then_reports_full_for_the_wrapper_to_cull() {
        let key = IdentitySigningPublicKey::new(crate::crypto::ed25519_public_key(
            &crate::crypto::Ed25519SecretKey::new([0x21; 32]),
        ));
        let mut table = HeapReceiptTable::default();
        for i in 0..DEFAULT_MAX_OUTSTANDING_RECEIPTS {
            let receipt = OutstandingReceipt {
                packet_hash: PacketHash::new([(i % 251) as u8; 32]),
                command_id: CommandId(i as u64),
                kind: ReceiptKind::SendSinglePacket,
                peer_signing_key: key,
                sent_at: InstantMillis(0),
                timeout_at: InstantMillis(7_000),
            };
            assert_eq!(table.push(receipt), Ok(i));
        }
        let overflow = OutstandingReceipt {
            packet_hash: PacketHash::new([0xFF; 32]),
            command_id: CommandId(9_999),
            kind: ReceiptKind::SendSinglePacket,
            peer_signing_key: key,
            sent_at: InstantMillis(0),
            timeout_at: InstantMillis(7_000),
        };
        assert_eq!(table.push(overflow), Err(TrackReceiptError::TableFull));
        assert_eq!(table.len(), DEFAULT_MAX_OUTSTANDING_RECEIPTS);
    }
}