prns-runtime-tokio 0.3.4

Tokio host runtime for Personal Reticulum
Documentation
use tokio::sync::mpsc::{self, UnboundedReceiver};

use crate::engine::RequestResponseTimeout;
use crate::engine::Settlement;
use crate::manifold::compression;
use crate::manifold::driver::{HostCommand, HostResourceMetadata};
use crate::routing::links::request::{
    parse_response_plaintext, write_packed_binary_header, RequestId, MAX_PACKED_BINARY_HEADER_LEN,
    RESPONSE_WIRE_OVERHEAD,
};
use crate::routing::links::LinkId;
use crate::routing::request_handlers::RequestPathHash;
use crate::runtime::request_endpoints::RespondToken;
use crate::units::{ByteLimit, DurationMillis, RttMillis};

use super::super::PrnsNodeHandle;
use super::{RequestOptions, RESPONSE_PACKET_CEILING};
use prns_core::rncp::parse_file_metadata;

static MULTI_SEGMENT_STATIC_RESPONSE: [u8; super::STATIC_RESPONSE_SEGMENT_BYTES * 2 + 33_333] =
    [0x42; super::STATIC_RESPONSE_SEGMENT_BYTES * 2 + 33_333];

fn handle() -> (PrnsNodeHandle, UnboundedReceiver<HostCommand>) {
    let (commands, command_rx) = mpsc::unbounded_channel();
    (PrnsNodeHandle::over(commands), command_rx)
}

#[tokio::test]
async fn request_emits_a_request_any_and_returns_the_response_with_its_rtt() {
    let (handle, mut command_rx) = handle();
    let link = LinkId::new([5; 16]);
    let path_hash = RequestPathHash::new([0x44; 16]);

    let requesting = tokio::spawn(async move { handle.request(link, path_hash, b"ping").await });

    let HostCommand::RequestAny(request) = command_rx.recv().await.unwrap() else {
        panic!("request issues a RequestAny host command");
    };
    assert_eq!(request.link_id, link);
    assert_eq!(request.path_hash, path_hash);
    assert_eq!(request.data.as_slice(), &b"ping"[..]);
    assert_eq!(
        request.response_timeout,
        RequestResponseTimeout::LinkDefault
    );
    assert_eq!(request.maximum_response_bytes, ByteLimit::Unlimited);
    request
        .completion
        .send(Ok((b"pong".to_vec(), RttMillis::new(42))))
        .unwrap();

    let (data, rtt) = requesting.await.unwrap().unwrap();
    assert_eq!(data, b"pong");
    assert_eq!(rtt, RttMillis::new(42));
}

#[tokio::test]
async fn request_options_preserve_the_response_ceiling() {
    let (handle, mut command_rx) = handle();
    let link = LinkId::new([7; 16]);
    let path_hash = RequestPathHash::new([0x46; 16]);
    let options = RequestOptions {
        response_timeout: RequestResponseTimeout::Exact(DurationMillis(45_000)),
        maximum_response_bytes: ByteLimit::Maximum(8_192),
    };

    let requesting = tokio::spawn(async move {
        handle
            .request_with_options(link, path_hash, b"bounded", options)
            .await
    });
    let HostCommand::RequestAny(request) = command_rx.recv().await.unwrap() else {
        panic!("request issues a RequestAny host command");
    };
    assert_eq!(request.response_timeout, options.response_timeout);
    assert_eq!(
        request.maximum_response_bytes,
        options.maximum_response_bytes
    );
    request
        .completion
        .send(Ok((b"done".to_vec(), RttMillis::new(42))))
        .unwrap();
    assert_eq!(requesting.await.unwrap().unwrap().0, b"done");
}

#[tokio::test]
async fn request_preserves_an_explicit_response_timeout() {
    let (handle, mut command_rx) = handle();
    let link = LinkId::new([6; 16]);
    let path_hash = RequestPathHash::new([0x45; 16]);
    let timeout = RequestResponseTimeout::Exact(DurationMillis(45_000));

    let requesting = tokio::spawn(async move {
        handle
            .request_with_response_timeout(link, path_hash, b"slow", timeout)
            .await
    });
    let HostCommand::RequestAny(request) = command_rx.recv().await.unwrap() else {
        panic!("request issues a RequestAny host command");
    };
    assert_eq!(request.response_timeout, timeout);
    request
        .completion
        .send(Ok((b"done".to_vec(), RttMillis::new(42))))
        .unwrap();
    assert_eq!(requesting.await.unwrap().unwrap().0, b"done");
}

#[tokio::test]
async fn respond_packed_returns_the_links_round_trip() {
    let (handle, _command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([1; 16]),
        request_id: RequestId([2; 16]),
        rtt: RttMillis::new(99),
    };
    assert_eq!(
        handle.respond_packed(token, b"answer"),
        Some(RttMillis::new(99)),
        "respond surfaces the rtt the request arrived on",
    );
}

#[tokio::test]
async fn a_large_response_carries_a_bz2_candidate() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([1; 16]),
        request_id: RequestId([2; 16]),
        rtt: RttMillis::new(50),
    };
    let body = std::vec![42u8; RESPONSE_PACKET_CEILING + 4096];
    assert_eq!(
        handle.respond_packed(token, &body),
        Some(RttMillis::new(50))
    );
    let Some(HostCommand::RespondAny(respond)) = command_rx.recv().await else {
        panic!("expected a RespondAny command");
    };
    let (enclosed_request, enclosed_body) =
        parse_response_plaintext(respond.packed.as_slice()).expect("stock RNS response envelope");
    assert_eq!(enclosed_request, token.request_id);
    assert_eq!(enclosed_body, body);
    assert_eq!(
        respond
            .compressed_candidate
            .as_ref()
            .map(|candidate| candidate.as_slice().to_vec()),
        compression::compress_if_smaller(respond.packed.as_slice()),
        "a response past the packet ceiling rides a bz2 candidate matching the codec",
    );
    assert!(respond.compressed_candidate.is_some(), "a run compresses");
}

#[tokio::test]
async fn a_packet_sized_response_skips_compression() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([1; 16]),
        request_id: RequestId([2; 16]),
        rtt: RttMillis::new(50),
    };
    let body = std::vec![42u8; RESPONSE_PACKET_CEILING];
    handle.respond_packed(token, &body);
    let Some(HostCommand::RespondAny(respond)) = command_rx.recv().await else {
        panic!("expected a RespondAny command");
    };
    assert!(
        respond.compressed_candidate.is_none(),
        "a response that fits a packet never builds a candidate the rung would discard",
    );
}

#[tokio::test]
async fn a_settled_resource_response_waits_for_its_proof() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([7; 16]),
        request_id: RequestId([8; 16]),
        rtt: RttMillis::new(33),
    };
    let body = std::vec![0xA5u8; RESPONSE_PACKET_CEILING + 1024];
    let responding =
        tokio::spawn(async move { handle.respond_owned_packed_settled(token, body).await });

    let Some(HostCommand::RespondAny(mut response)) = command_rx.recv().await else {
        panic!("a resource response reaches the host driver");
    };
    assert!(
        !responding.is_finished(),
        "the route remains occupied until Resource proof settlement"
    );
    response
        .completion
        .take()
        .expect("settled response carries completion")
        .send(Settlement::Respond(Ok(())))
        .expect("route awaits completion");
    assert_eq!(responding.await.unwrap().unwrap(), RttMillis::new(33));
}

#[tokio::test]
async fn respond_bytes_adds_message_pack_binary_framing() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([9; 16]),
        request_id: RequestId([10; 16]),
        rtt: RttMillis::new(34),
    };
    assert_eq!(
        handle.respond_bytes(token, b"hello"),
        Some(RttMillis::new(34))
    );
    let Some(HostCommand::RespondAny(respond)) = command_rx.recv().await else {
        panic!("expected a RespondAny command");
    };
    assert_eq!(
        respond.packed.as_slice(),
        &[0xC4, 5, b'h', b'e', b'l', b'l', b'o']
    );
}

#[tokio::test]
async fn respond_bytes_streaming_reads_a_bounded_source_into_a_response_resource() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([11; 16]),
        request_id: RequestId([12; 16]),
        rtt: RttMillis::new(35),
    };
    let bytes = std::vec![0xA5; RESPONSE_PACKET_CEILING + 1];
    let byte_len = bytes.len() as u64;
    let responding = tokio::spawn(async move {
        handle
            .respond_bytes_streaming(token, byte_len, std::io::Cursor::new(bytes))
            .await
    });
    let Some(HostCommand::SendResourceSegment(segment)) = command_rx.recv().await else {
        panic!("expected a response resource segment");
    };
    let mut binary_header = [0u8; MAX_PACKED_BINARY_HEADER_LEN];
    let binary_header_len =
        write_packed_binary_header(RESPONSE_PACKET_CEILING + 1, &mut binary_header).unwrap();
    assert_eq!(segment.request_id, Some(token.request_id));
    assert_eq!(
        segment.data.len(),
        RESPONSE_WIRE_OVERHEAD + binary_header_len + RESPONSE_PACKET_CEILING + 1
    );
    let (request_id, packed) = parse_response_plaintext(segment.data.as_slice()).unwrap();
    assert_eq!(request_id, token.request_id);
    assert_eq!(
        &packed[..binary_header_len],
        &binary_header[..binary_header_len]
    );
    assert!(packed[binary_header_len..].iter().all(|byte| *byte == 0xA5));
    segment
        .completion
        .send(Settlement::Respond(Ok(())))
        .unwrap();
    assert_eq!(responding.await.unwrap().unwrap(), token.rtt);
}

#[tokio::test]
async fn static_file_response_waits_for_each_proof_and_bounds_each_window() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([13; 16]),
        request_id: RequestId([14; 16]),
        rtt: RttMillis::new(36),
    };
    let bytes: &'static [u8] = &MULTI_SEGMENT_STATIC_RESPONSE;
    let responding = tokio::spawn(async move {
        handle
            .respond_static_file_settled(token, "source.zip", bytes)
            .await
    });

    let mut expected_index = 1u64;
    let mut total_segments = None;
    loop {
        let Some(HostCommand::SendResourceSegment(segment)) = command_rx.recv().await else {
            panic!("expected a response resource segment");
        };
        assert_eq!(segment.request_id, Some(token.request_id));
        assert_eq!(segment.segment_index, expected_index);
        assert!(
            segment.data.len() <= super::STATIC_RESPONSE_SEGMENT_BYTES,
            "one live plaintext window remains bounded"
        );
        assert!(
            segment.data.as_slice().iter().all(|byte| *byte == 0x42),
            "every window carries bare file bytes with no envelope or binary header"
        );
        if expected_index == 1 {
            let HostResourceMetadata::Packed(metadata) = &segment.metadata else {
                panic!("first segment carries filename metadata");
            };
            assert_eq!(
                parse_file_metadata(metadata.as_slice()).unwrap(),
                b"source.zip"
            );
            total_segments = Some(segment.total_segments);
            assert!(segment.total_segments > 2);
        } else {
            assert!(matches!(
                segment.metadata,
                HostResourceMetadata::SentInFirstSegment { .. }
            ));
            assert_eq!(Some(segment.total_segments), total_segments);
        }
        assert!(
            command_rx.try_recv().is_err(),
            "the next window is not queued before this proof"
        );
        segment
            .completion
            .send(Settlement::Respond(Ok(())))
            .unwrap();
        if Some(expected_index) == total_segments {
            break;
        }
        expected_index += 1;
    }

    assert_eq!(responding.await.unwrap().unwrap(), token.rtt);
}

#[tokio::test]
async fn an_open_file_response_streams_from_its_handle_with_filename_metadata() {
    let (handle, mut command_rx) = handle();
    let token = RespondToken {
        link_id: LinkId::new([17; 16]),
        request_id: RequestId([18; 16]),
        rtt: RttMillis::new(51),
    };
    let expected = std::fs::read("Cargo.toml").unwrap();
    let source = std::fs::File::open("Cargo.toml").unwrap();
    let byte_len = expected.len() as u64;
    let responding = tokio::spawn(async move {
        handle
            .respond_open_file_settled(token, "Cargo.toml", source, byte_len)
            .await
    });

    let Some(HostCommand::SendResourceSegment(segment)) = command_rx.recv().await else {
        panic!("expected an open-file response resource segment");
    };
    assert_eq!(segment.request_id, Some(token.request_id));
    assert_eq!(segment.segment_index, 1);
    assert_eq!(segment.total_segments, 1);
    assert_eq!(segment.data.as_slice(), expected.as_slice());
    let HostResourceMetadata::Packed(metadata) = &segment.metadata else {
        panic!("the segment carries filename metadata");
    };
    assert_eq!(
        parse_file_metadata(metadata.as_slice()).unwrap(),
        b"Cargo.toml"
    );
    segment
        .completion
        .send(Settlement::Respond(Ok(())))
        .unwrap();

    assert_eq!(responding.await.unwrap().unwrap(), token.rtt);
}