prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::*;
use crate::engine::test_support::*;
use crate::engine::IngestIo;
use crate::engine::{Directive, EngineReaction, EngineState, IngestPacketOutcome, RatchetPolicy};
use crate::identity::in_memory::InMemoryNodeIdentity;
use crate::identity::IdentityHash;
use crate::interfaces::AttachedInterfaces;
use crate::interfaces::{InboundPacket, InterfaceId};
use crate::routing::dedup::PacketHash;
use crate::routing::delivery::Delivery;
use crate::routing::links::table::LinkActivation;
use crate::routing::proof::{
    ProofObligation, ProofRequest, EXPLICIT_PROOF_WIRE_LEN, LINK_PROOF_WIRE_LEN,
};
use crate::routing::upstream_app_destinations::LinkRequestPolicy;
use crate::routing::upstream_app_destinations::ProofStrategy;
use crate::wire::{WirePacketHeader, BROADCAST_MTU, HEADER_MIN_LEN};

#[test]
fn write_proof_is_byte_identical_to_the_rns_1_4_2_implicit_proof() {
    let mut state: EngineState<TestStorageLayout> = EngineState::<TestStorageLayout>::default();
    let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key());
    let held = state.hold_identity(fixed_secret_key()).unwrap();
    let destination = state
        .register_single_destination(
            &held,
            "personal",
            &["node"],
            b"",
            ProofStrategy::ProveAll,
            LinkRequestPolicy::AcceptAll,
            RatchetPolicy::NoRatchets,
        )
        .unwrap();

    let mut raw = sealed_single_packet(&identity, destination, b"proof-parity");
    assert_eq!(raw, bytes_from_hex(RNS_1_4_2_SEALED_FOR_PROOF));

    let outcome = state.ingest_packet_with(
        plain_data_packet(&mut raw),
        &mut |_| {},
        AttachedInterfaces::new(&transporting_interfaces()),
        &mut |_| {},
        None,
    );
    let IngestPacketOutcome::Delivery {
        proof: ProofObligation::Owed(owed),
        ..
    } = outcome
    else {
        panic!("a ProveAll delivery owes a proof");
    };

    let mut buf = [0u8; BROADCAST_MTU];
    let written = state.write_proof(&owed, &mut buf).unwrap();
    assert_eq!(
        &buf[..written],
        bytes_from_hex(RNS_1_4_2_IMPLICIT_PROOF).as_slice()
    );
}

#[test]
fn explicit_non_link_proofs_name_the_proven_packet_hash() {
    let mut state: EngineState<TestStorageLayout> = EngineState::default();
    state.set_protocol_policy(crate::engine::EngineProtocolPolicy {
        proof_form: ProofForm::Explicit,
        ..Default::default()
    });
    let packet_hash = PacketHash::new([0xA5; PACKET_HASH_LEN]);
    let signature = Ed25519Signature([0x5A; 64]);
    let mut buf = [0u8; EXPLICIT_PROOF_WIRE_LEN];
    let written = state
        .write_signed_proof(&packet_hash, &signature, &mut buf)
        .unwrap();
    assert_eq!(written, EXPLICIT_PROOF_WIRE_LEN);
    let (_, payload) = WirePacketHeader::parse(&buf).unwrap();
    assert_eq!(&payload[..PACKET_HASH_LEN], packet_hash.as_bytes());
    assert_eq!(&payload[PACKET_HASH_LEN..], &signature.0);
}

fn prove_if_state() -> (
    EngineState<TestStorageLayout>,
    InMemoryNodeIdentity,
    crate::wire::DestinationHash,
) {
    let mut state: EngineState<TestStorageLayout> = EngineState::<TestStorageLayout>::default();
    let identity = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key());
    let held = state.hold_identity(fixed_secret_key()).unwrap();
    let destination = state
        .register_single_destination(
            &held,
            "personal",
            &["node"],
            b"",
            ProofStrategy::ProveIf,
            LinkRequestPolicy::AcceptAll,
            RatchetPolicy::NoRatchets,
        )
        .unwrap();
    (state, identity, destination)
}

#[test]
fn a_prove_if_delivery_defers_the_proof_to_the_app() {
    let (mut state, identity, destination) = prove_if_state();
    let mut raw = sealed_single_packet(&identity, destination, b"prove-if");
    let IngestPacketOutcome::Delivery {
        delivery: Delivery::Single(single),
        proof: ProofObligation::OwedIfApp(_),
    } = state.ingest_packet_with(
        plain_data_packet(&mut raw),
        &mut |_| {},
        AttachedInterfaces::new(&transporting_interfaces()),
        &mut |_| {},
        None,
    )
    else {
        panic!("a ProveIf delivery defers its proof to the app");
    };
    assert_eq!(
        single.plaintext, b"prove-if",
        "the deferred decision sees the decrypted content",
    );
}

fn prove_if_proof_directive(
    decide: impl FnMut(&ProofRequest) -> bool,
) -> (bool, std::vec::Vec<u8>) {
    let (mut state, identity, destination) = prove_if_state();
    let mut raw = sealed_single_packet(&identity, destination, b"prove-if");
    let mut decide = decide;
    let mut seen = std::vec::Vec::new();
    let mut proved = false;
    state.ingest_packet_into(
        InboundPacket {
            arrived_at: InstantMillis(1_000),
            source_interface: InterfaceId::new([0xEE; 8]),
            bytes: &mut raw,
        },
        IngestIo {
            interfaces: AttachedInterfaces::new(&transporting_interfaces()),
            now: InstantMillis(1_000),
            fill_entropy: &mut |bytes| bytes.fill(0),
            should_prove: &mut |request| {
                seen = request.plaintext.to_vec();
                decide(request)
            },
            should_accept_resource: &mut |_: &crate::routing::links::resources::ResourceOffer| {
                false
            },
            sink: &mut |reaction| {
                if let EngineReaction::Directive(Directive::Send { .. }) = reaction {
                    proved = true;
                }
            },
        },
    );
    (proved, seen)
}

#[test]
fn the_app_decider_gates_the_prove_if_proof() {
    let (proved, seen) = prove_if_proof_directive(|_| true);
    assert!(
        proved,
        "the decider agreed, so the manifold answers a proof"
    );
    assert_eq!(seen, b"prove-if", "the decider sees the decrypted content");

    let (proved, _) = prove_if_proof_directive(|_| false);
    assert!(!proved, "the decider declined, so no proof goes out");
}

#[test]
fn write_proof_for_an_unheld_identity_reports_it() {
    let state: EngineState<TestStorageLayout> = EngineState::<TestStorageLayout>::default();
    let owed = ProofOwed {
        packet_hash: PacketHash::new([0xAA; 32]),
        identity: IdentityHash::new([0x4c; 16]),
    };
    let mut buf = [0u8; BROADCAST_MTU];
    assert_eq!(
        state.write_proof(&owed, &mut buf),
        Err(WriteProofError::IdentityNotHeld),
    );
}

#[test]
fn write_proof_into_a_short_buffer_reports_it() {
    let mut state: EngineState<TestStorageLayout> = EngineState::<TestStorageLayout>::default();
    let held = state.hold_identity(fixed_secret_key()).unwrap();
    let owed = ProofOwed {
        packet_hash: PacketHash::new([0xAA; 32]),
        identity: held,
    };
    let mut buf = [0u8; 8];
    assert_eq!(
        state.write_proof(&owed, &mut buf),
        Err(WriteProofError::Serialize(WireError::BufferTooShort)),
    );
}

#[test]
fn an_initiator_channel_ack_is_signed_by_the_link_key() {
    use crate::crypto::{
        ed25519_public_key, ed25519_verify, x25519_diffie_hellman, Ed25519PublicKey,
        Ed25519SecretKey, X25519PublicKey, X25519SecretKey,
    };
    use crate::engine::CommandId;
    use crate::routing::links::table::InitiatedLink;
    use crate::routing::links::{LinkId, LinkKey};

    let mut state = EngineState::<TestStorageLayout>::default();
    let link_id = LinkId::new([0x5C; 16]);
    let link_signing = Ed25519SecretKey::new([0x42; 32]);
    let link_signing_public = ed25519_public_key(&link_signing);
    state
        .links
        .track_initiated(InitiatedLink {
            link_id,
            destination: DestinationHash::new([0x77; 16]),
            expected_hops: 1,
            mode: crate::routing::links::LinkMode::Aes256Cbc,
            initiator_secret: X25519SecretKey::new([0x33; 32]),
            link_signing,
            requested_at: InstantMillis(0),
            timeout_at: InstantMillis(5_000),
            command_id: CommandId(1),
        })
        .unwrap();
    let shared = x25519_diffie_hellman(
        &X25519SecretKey::new([0x33; 32]),
        &X25519PublicKey([0x44; 32]),
    );
    state
        .links
        .activate_initiated(
            &link_id,
            LinkKey::derive(&link_id, &shared),
            &LinkActivation {
                received_hops: 1,
                rtt: crate::units::RttMillis::new(250),
                mtu: BROADCAST_MTU,
                attached_interface: InterfaceId::new([0xEE; 8]),
                peer_signing: Ed25519PublicKey([0x99; 32]),
            },
            InstantMillis(1_000),
        )
        .unwrap();

    let packet_hash = PacketHash::new([0xAB; 32]);
    let mut buf = [0u8; BROADCAST_MTU];
    let written = state
        .write_channel_ack(&link_id, &packet_hash, &mut buf)
        .unwrap();
    assert_eq!(written, LINK_PROOF_WIRE_LEN);
    assert_eq!(
        &buf[HEADER_MIN_LEN..HEADER_MIN_LEN + PACKET_HASH_LEN],
        packet_hash.as_bytes(),
        "the proof names the packet it acks",
    );
    let signature = Ed25519Signature(
        buf[HEADER_MIN_LEN + PACKET_HASH_LEN..LINK_PROOF_WIRE_LEN]
            .try_into()
            .unwrap(),
    );
    ed25519_verify(&link_signing_public, packet_hash.as_bytes(), &signature)
        .expect("the initiator signs the ack with its own ephemeral link key");
}

#[test]
fn a_responder_channel_ack_is_signed_by_the_held_identity() {
    use crate::crypto::{
        ed25519_verify, x25519_diffie_hellman, Ed25519PublicKey, X25519PublicKey, X25519SecretKey,
    };
    use crate::identity::IdentitySigner;
    use crate::routing::links::table::RespondingLink;
    use crate::routing::links::{LinkId, LinkKey};

    let mut state = EngineState::<TestStorageLayout>::default();
    let identity = state.hold_identity(fixed_secret_key()).unwrap();
    let signer = InMemoryNodeIdentity::from_secret_key_bytes(&fixed_secret_key());
    let signing_public = *signer.signing_public_key().as_ed25519();

    let link_id = LinkId::new([0x6D; 16]);
    let shared = x25519_diffie_hellman(
        &X25519SecretKey::new([0x55; 32]),
        &X25519PublicKey([0x66; 32]),
    );
    state
        .links
        .track_responding(RespondingLink {
            link_id,
            key: LinkKey::derive(&link_id, &shared),
            requested_at: InstantMillis(0),
            timeout_at: InstantMillis(5_000),
            mtu: BROADCAST_MTU,
            initiator_signing: Ed25519PublicKey([0x99; 32]),
            destination: DestinationHash::new([0x77; 16]),
            identity,
            proof_strategy: ProofStrategy::ProveAll,
        })
        .unwrap();
    state
        .links
        .activate_responding(
            &link_id,
            crate::units::RttMillis::new(250),
            InterfaceId::new([0xEE; 8]),
            InstantMillis(1_000),
        )
        .unwrap();

    let packet_hash = PacketHash::new([0xCD; 32]);
    let mut buf = [0u8; BROADCAST_MTU];
    let written = state
        .write_channel_ack(&link_id, &packet_hash, &mut buf)
        .unwrap();
    assert_eq!(written, LINK_PROOF_WIRE_LEN);
    let signature = Ed25519Signature(
        buf[HEADER_MIN_LEN + PACKET_HASH_LEN..LINK_PROOF_WIRE_LEN]
            .try_into()
            .unwrap(),
    );
    ed25519_verify(&signing_public, packet_hash.as_bytes(), &signature)
        .expect("the responder signs the ack with the destination identity it answers for");
}

#[test]
fn a_channel_ack_for_an_inactive_link_reports_it() {
    use crate::routing::links::LinkId;

    let state = EngineState::<TestStorageLayout>::default();
    let mut buf = [0u8; BROADCAST_MTU];
    assert_eq!(
        state.write_channel_ack(
            &LinkId::new([0x01; 16]),
            &PacketHash::new([0u8; 32]),
            &mut buf
        ),
        Err(WriteChannelAckError::LinkNotActive),
    );
}