prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::interfaces::InterfaceId;
use crate::routing::announce::{
    write_relayed_path_response_wire_packet, write_retransmitted_announce_wire_packet, Announce,
};
use crate::wire::{TransportId, WireError};

#[derive(Debug)]
pub struct ReemitAnnounce<'a> {
    pub announce: Announce<'a>,
    pub emit_hops: u8,
    pub via: TransportId,
    pub target: InterfaceId,
    pub is_path_response: bool,
}

impl ReemitAnnounce<'_> {
    pub fn to_wire(&self, buf: &mut [u8]) -> Result<usize, WireError> {
        if self.is_path_response {
            write_relayed_path_response_wire_packet(&self.announce, self.emit_hops, self.via, buf)
        } else {
            write_retransmitted_announce_wire_packet(&self.announce, self.emit_hops, self.via, buf)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::test_support::{bytes_from_hex, RNS_1_4_2_ANNOUNCE};
    use crate::wire::{
        DestinationType, PacketType, PropagationType, WireContext, WirePacketHeader, HEADER_MAX_LEN,
    };

    const TEST_VIA: TransportId = TransportId::new([0x7A; 16]);

    fn iface(byte: u8) -> InterfaceId {
        InterfaceId::new([byte; 8])
    }

    #[test]
    fn reemit_announce_to_wire_produces_a_well_formed_wire_packet() {
        let raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
        let (orig_header, orig_payload) = WirePacketHeader::parse(&raw).unwrap();
        let announce = Announce::from_wire(&orig_header, orig_payload).unwrap();

        let directive = ReemitAnnounce {
            announce,
            emit_hops: orig_header.hops + 1,
            via: TEST_VIA,
            target: iface(0xAA),
            is_path_response: false,
        };

        let mut buf = [0u8; 500];
        let n = directive.to_wire(&mut buf).unwrap();
        let wire = &buf[..n];

        let (parsed_header, parsed_payload) = WirePacketHeader::parse(wire).unwrap();
        assert_eq!(parsed_header.packet_type, PacketType::Announce);
        assert_eq!(parsed_header.destination_type, DestinationType::Single);
        assert_eq!(parsed_header.propagation, PropagationType::Transport);
        assert_eq!(parsed_header.transport_id, Some(TEST_VIA));
        assert_eq!(parsed_header.hops, orig_header.hops + 1);
        assert_eq!(parsed_header.address, orig_header.address);
        assert_eq!(parsed_header.context, WireContext::None);
        assert_eq!(parsed_payload, orig_payload);
    }

    #[test]
    fn a_directed_path_response_reemit_carries_the_path_response_context() {
        let raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
        let (orig_header, orig_payload) = WirePacketHeader::parse(&raw).unwrap();
        let announce = Announce::from_wire(&orig_header, orig_payload).unwrap();

        let directive = ReemitAnnounce {
            announce,
            emit_hops: orig_header.hops + 1,
            via: TEST_VIA,
            target: iface(0xAA),
            is_path_response: true,
        };

        let mut buf = [0u8; 500];
        let n = directive.to_wire(&mut buf).unwrap();
        let (parsed_header, parsed_payload) = WirePacketHeader::parse(&buf[..n]).unwrap();

        assert_eq!(parsed_header.context, WireContext::PathResponse);
        assert_eq!(parsed_header.propagation, PropagationType::Transport);
        assert_eq!(parsed_header.transport_id, Some(TEST_VIA));
        assert_eq!(parsed_header.packet_type, PacketType::Announce);
        assert_eq!(parsed_header.hops, orig_header.hops + 1);
        assert_eq!(parsed_header.address, orig_header.address);
        assert_eq!(parsed_payload, orig_payload);
    }

    #[test]
    fn to_wire_with_buffer_too_short_returns_buffer_too_short() {
        let raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
        let (orig_header, orig_payload) = WirePacketHeader::parse(&raw).unwrap();
        let announce = Announce::from_wire(&orig_header, orig_payload).unwrap();

        let directive = ReemitAnnounce {
            announce,
            emit_hops: 1,
            via: TEST_VIA,
            target: iface(0xAB),
            is_path_response: false,
        };

        let mut tiny_buf = [0u8; 8];
        assert!(matches!(
            directive.to_wire(&mut tiny_buf),
            Err(WireError::BufferTooShort)
        ));
    }

    #[test]
    fn to_wire_with_exactly_sized_buffer_succeeds() {
        let raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
        let (orig_header, orig_payload) = WirePacketHeader::parse(&raw).unwrap();
        let announce = Announce::from_wire(&orig_header, orig_payload).unwrap();
        let exact_len = HEADER_MAX_LEN + announce.wire_bytes();

        let directive = ReemitAnnounce {
            announce,
            emit_hops: 9,
            via: TEST_VIA,
            target: iface(0xAC),
            is_path_response: false,
        };

        let mut exact_buf = std::vec![0u8; exact_len];
        let written = directive.to_wire(&mut exact_buf).unwrap();
        assert_eq!(written, exact_len);

        let (header, payload) = WirePacketHeader::parse(&exact_buf).unwrap();
        assert_eq!(header.hops, 9);
        assert_eq!(header.address, orig_header.address);
        assert_eq!(payload, orig_payload);
    }

    #[test]
    fn to_wire_output_round_trips_to_an_equivalent_announce() {
        let raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
        let (orig_header, orig_payload) = WirePacketHeader::parse(&raw).unwrap();
        let orig_announce = Announce::from_wire(&orig_header, orig_payload).unwrap();

        let directive = ReemitAnnounce {
            announce: orig_announce.clone(),
            emit_hops: 5,
            via: TEST_VIA,
            target: iface(0x42),
            is_path_response: false,
        };

        let mut buf = [0u8; 500];
        let n = directive.to_wire(&mut buf).unwrap();
        let (re_header, re_payload) = WirePacketHeader::parse(&buf[..n]).unwrap();
        let re_announce = Announce::from_wire(&re_header, re_payload).unwrap();

        assert_eq!(re_header.hops, 5);
        assert_eq!(re_announce, orig_announce);
    }
}

#[cfg_attr(mutants, mutants::skip)]
#[cfg(kani)]
mod kani_proofs {
    use super::*;
    use crate::crypto::{Ed25519PublicKey, Ed25519Signature, X25519PublicKey};
    use crate::identity::{IdentityEncryptionPublicKey, IdentitySigningPublicKey};
    use crate::routing::announce::{
        AnnounceId, DottedNameHash, IdentityPublicKeys, ANNOUNCE_FIXED_FIELDS_LEN,
    };
    use crate::wire::{
        ContextFlag, DestinationHash, DestinationType, IfacFlag, PacketType, PropagationType,
        WireContext, WirePacketHeader, HEADER_MAX_LEN,
    };

    const APP_DATA_LEN: usize = 2;
    const ANNOUNCE_WIRE_LEN: usize = ANNOUNCE_FIXED_FIELDS_LEN + APP_DATA_LEN;
    const EXACT_REEMIT_LEN: usize = HEADER_MAX_LEN + ANNOUNCE_WIRE_LEN;
    static APP_DATA: [u8; APP_DATA_LEN] = [0xA5, 0x5A];

    fn arbitrary_announce() -> Announce<'static> {
        Announce {
            destination: DestinationHash::new(kani::any()),
            public_keys: IdentityPublicKeys {
                encryption: IdentityEncryptionPublicKey::new(X25519PublicKey(kani::any())),
                signing: IdentitySigningPublicKey::new(Ed25519PublicKey(kani::any())),
            },
            dotted_name_hash: DottedNameHash::new(kani::any()),
            announce_id: AnnounceId::from_wire(kani::any()),
            ratchet: None,
            signature: Ed25519Signature(kani::any()),
            app_data: &APP_DATA,
        }
    }

    #[kani::proof]
    fn reemit_announce_exact_buffer_serializes_header_and_payload_length() {
        let announce = arbitrary_announce();
        let emit_hops: u8 = kani::any();
        let via = TransportId::new(kani::any());
        let target = InterfaceId::new(kani::any());
        let directive = ReemitAnnounce {
            announce: announce.clone(),
            emit_hops,
            via,
            target,
            is_path_response: false,
        };

        let mut buf = [0u8; EXACT_REEMIT_LEN];
        let written = directive.to_wire(&mut buf).unwrap();
        assert_eq!(written, EXACT_REEMIT_LEN);

        let (header, payload) = WirePacketHeader::parse(&buf).unwrap();
        assert_eq!(header.ifac_flag, IfacFlag::Open);
        assert_eq!(header.context_flag, ContextFlag::Unset);
        assert_eq!(header.propagation, PropagationType::Transport);
        assert_eq!(header.destination_type, DestinationType::Single);
        assert_eq!(header.packet_type, PacketType::Announce);
        assert_eq!(header.hops, emit_hops);
        assert_eq!(header.transport_id, Some(via));
        assert_eq!(
            DestinationHash::from_address(header.address),
            announce.destination
        );
        assert_eq!(header.context, WireContext::None);
        assert_eq!(payload.len(), ANNOUNCE_WIRE_LEN);
        assert_eq!(directive.target, target);
    }

    #[kani::proof]
    fn reemit_announce_short_buffer_rejects_before_a_full_packet_is_written() {
        let announce = arbitrary_announce();
        let directive = ReemitAnnounce {
            announce,
            emit_hops: kani::any(),
            via: TransportId::new(kani::any()),
            target: InterfaceId::new(kani::any()),
            is_path_response: false,
        };

        let mut buf = [0u8; EXACT_REEMIT_LEN - 1];
        assert_eq!(directive.to_wire(&mut buf), Err(WireError::BufferTooShort));
    }
}