lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

reticulum.rs (15151B)


      1 use futures::executor::block_on;
      2 use radroots_event::{SignedEvent, wire::Nip01EventWire};
      3 use radroots_transport::{
      4     DeliveryRequest, Error as TransportError, EventSink, EventSource, FetchRequest, Target,
      5     TargetSet, TransportId,
      6     capability::{Availability, Maturity},
      7     outcome::{DeliveryOutcomeKind, FetchTargetState},
      8     policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
      9     sink::DeliveryPayload,
     10     source::FetchBounds,
     11     target::{TargetLabel, TargetScope},
     12 };
     13 use radroots_transport_reticulum::{
     14     RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID,
     15     RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RETICULUM_V1_MAX_PAYLOAD_BYTES,
     16     RadrootsReticulumAgentEndpoint, RadrootsReticulumBehavior, RadrootsReticulumEndpoint,
     17     RadrootsReticulumError, RadrootsReticulumProfile, RadrootsReticulumTransport,
     18     ReticulumCapabilityReportV1, ReticulumDestinationV1, ReticulumDuplicateFragmentBehaviorV1,
     19     ReticulumFragmentIntegrityV1, ReticulumFragmentPolicyV1, ReticulumFragmentationModeV1,
     20     ReticulumGatewaySemanticsV1, ReticulumPayloadPolicyV1, ReticulumPrivacySemanticsV1,
     21     ReticulumRoutingMetadataV1,
     22 };
     23 
     24 fn signed_event() -> SignedEvent {
     25     let raw = r#"{"id":"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1700000001,"kind":0,"tags":[],"content":"{}","sig":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}"#;
     26     SignedEvent::from_wire_verified_id(Nip01EventWire::parse_json(raw).expect("wire event"), raw)
     27         .expect("signed event")
     28 }
     29 
     30 fn scope() -> TargetScope {
     31     TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope")
     32 }
     33 
     34 fn target() -> Target {
     35     ReticulumDestinationV1::local()
     36         .transport_target()
     37         .expect("Reticulum target")
     38 }
     39 
     40 fn target_set(target: Target) -> TargetSet {
     41     TargetSet::new(vec![target]).expect("target set")
     42 }
     43 
     44 fn delivery(target: Target) -> DeliveryRequest {
     45     DeliveryRequest::new(
     46         "reticulum-delivery",
     47         DeliveryPayload::new(signed_event()),
     48         target_set(target),
     49         SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()),
     50         1_800_000_200_000,
     51     )
     52     .expect("delivery request")
     53 }
     54 
     55 fn fetch(target: Target) -> FetchRequest {
     56     FetchRequest::new(
     57         "reticulum-fetch",
     58         target_set(target),
     59         FetchBounds::new(10, 1_800_000_200_000).expect("fetch bounds"),
     60     )
     61     .expect("fetch request")
     62 }
     63 
     64 #[test]
     65 fn payload_fragment_and_routing_policies_are_explicitly_inert() {
     66     let fragments = ReticulumFragmentPolicyV1::unsupported();
     67     assert_eq!(fragments.mode, ReticulumFragmentationModeV1::Unsupported);
     68     assert_eq!(fragments.max_fragment_count, 1);
     69     assert_eq!(
     70         fragments.max_reassembled_bytes,
     71         RETICULUM_V1_MAX_PAYLOAD_BYTES
     72     );
     73     assert_eq!(
     74         fragments.duplicate_fragment_behavior,
     75         ReticulumDuplicateFragmentBehaviorV1::Reject
     76     );
     77     assert_eq!(
     78         fragments.integrity_verification,
     79         ReticulumFragmentIntegrityV1::PayloadDigest
     80     );
     81 
     82     let payload = ReticulumPayloadPolicyV1::v1();
     83     assert_eq!(payload.max_payload_bytes, RETICULUM_V1_MAX_PAYLOAD_BYTES);
     84     assert_eq!(payload.fragment_policy, fragments);
     85 
     86     let routing = ReticulumRoutingMetadataV1::local();
     87     assert_eq!(routing.scope.as_str(), RADROOTS_RETICULUM_SCOPE_ID);
     88     assert_eq!(
     89         routing.gateway,
     90         ReticulumGatewaySemanticsV1::NoGatewayForwarding
     91     );
     92     assert_eq!(
     93         routing.privacy,
     94         ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly
     95     );
     96 
     97     let report = ReticulumCapabilityReportV1::unavailable_local();
     98     assert!(report.delivery_required);
     99     assert!(!report.fetch_required);
    100     assert!(!report.can_deliver);
    101     assert!(!report.can_fetch);
    102     assert!(!report.can_discover);
    103     assert!(!report.can_forward_gateway);
    104     assert!(!report.can_observe_receipts);
    105     assert_eq!(report.payload_policy, payload);
    106 }
    107 
    108 #[test]
    109 fn endpoint_and_agent_endpoint_values_are_strict_and_canonical() {
    110     let endpoint = RadrootsReticulumEndpoint::default();
    111     assert_eq!(endpoint.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI);
    112     assert_eq!(endpoint.to_string(), RADROOTS_RETICULUM_ENDPOINT_URI);
    113     assert_eq!(
    114         endpoint.clone().into_string(),
    115         RADROOTS_RETICULUM_ENDPOINT_URI
    116     );
    117     assert_eq!(
    118         RadrootsReticulumEndpoint::parse(RADROOTS_RETICULUM_ENDPOINT_URI).unwrap(),
    119         endpoint
    120     );
    121     for invalid in ["", " ", "RETICULUM:local", "reticulum:remote"] {
    122         assert_eq!(
    123             RadrootsReticulumEndpoint::parse(invalid).unwrap_err(),
    124             RadrootsReticulumError::InvalidEndpoint
    125         );
    126     }
    127 
    128     for valid in [
    129         "reticulum-agent:local-controller",
    130         "reticulum-agent://localhost:19999",
    131     ] {
    132         let agent = RadrootsReticulumAgentEndpoint::parse(valid).expect("agent endpoint");
    133         assert_eq!(agent.as_str(), valid);
    134         assert_eq!(agent.to_string(), valid);
    135         assert_eq!(agent.into_string(), valid);
    136     }
    137     for invalid in [
    138         "",
    139         " reticulum-agent:local",
    140         "reticulum-agent:local ",
    141         "reticulum-agent:local\n",
    142         "reticulum agent:local",
    143         "RETICULUM-AGENT:local",
    144         "reticulum-agent:",
    145     ] {
    146         assert_eq!(
    147             RadrootsReticulumAgentEndpoint::parse(invalid).unwrap_err(),
    148             RadrootsReticulumError::InvalidAgentEndpoint,
    149             "{invalid:?}"
    150         );
    151     }
    152 }
    153 
    154 #[test]
    155 fn destinations_preserve_transport_identity_scope_and_optional_labels() {
    156     let local = ReticulumDestinationV1::local();
    157     assert_eq!(local.uri().as_str(), RADROOTS_RETICULUM_ENDPOINT_URI);
    158     assert_eq!(local.routing().scope.as_str(), RADROOTS_RETICULUM_SCOPE_ID);
    159     assert_eq!(local.label(), None);
    160 
    161     let label = TargetLabel::parse("Local node").expect("label");
    162     let labeled = ReticulumDestinationV1::new(
    163         RADROOTS_RETICULUM_ENDPOINT_URI,
    164         TargetScope::parse("farm.mesh").expect("scope"),
    165         Some(label.clone()),
    166     )
    167     .expect("destination");
    168     assert_eq!(labeled.label(), Some(&label));
    169     assert_ne!(labeled.fingerprint(), local.fingerprint());
    170 
    171     let transport_target = labeled.transport_target().expect("transport target");
    172     assert_eq!(transport_target.kind(), &TransportId::RETICULUM);
    173     assert_eq!(
    174         ReticulumDestinationV1::from_target(&transport_target).expect("round trip"),
    175         labeled
    176     );
    177 
    178     let local_target = Target::local("local:memory").expect("local target");
    179     assert_eq!(
    180         ReticulumDestinationV1::from_target(&local_target).unwrap_err(),
    181         TransportError::InvalidTargetUri
    182     );
    183     let wrong_uri = Target::new_with_metadata(
    184         TransportId::RETICULUM,
    185         "reticulum:remote",
    186         Some(scope()),
    187         None,
    188     )
    189     .expect("wrong URI target");
    190     assert_eq!(
    191         ReticulumDestinationV1::from_target(&wrong_uri).unwrap_err(),
    192         TransportError::InvalidTargetUri
    193     );
    194     let missing_scope =
    195         Target::new(TransportId::RETICULUM, RADROOTS_RETICULUM_ENDPOINT_URI).expect("target");
    196     assert_eq!(
    197         ReticulumDestinationV1::from_target(&missing_scope).unwrap_err(),
    198         TransportError::EmptyTargetScope
    199     );
    200 }
    201 
    202 #[test]
    203 fn profiles_expose_canonical_preview_configuration() {
    204     let default = RadrootsReticulumProfile::default();
    205     assert_eq!(default.profile_id(), "transport.reticulum.default");
    206     assert_eq!(default.endpoint().as_str(), RADROOTS_RETICULUM_ENDPOINT_URI);
    207     assert_eq!(default.scope().as_str(), RADROOTS_RETICULUM_SCOPE_ID);
    208     assert_eq!(default.agent_endpoint(), None);
    209     assert_eq!(
    210         default.behavior(),
    211         RadrootsReticulumBehavior::RejectDeliveryAttempts
    212     );
    213     assert_eq!(
    214         default.destination(),
    215         &default.capability_report().destination
    216     );
    217 
    218     assert_eq!(
    219         RadrootsReticulumProfile::new(
    220             "",
    221             RadrootsReticulumEndpoint::default(),
    222             scope(),
    223             None,
    224             RadrootsReticulumBehavior::RejectDeliveryAttempts,
    225         )
    226         .unwrap_err(),
    227         RadrootsReticulumError::InvalidProfileId
    228     );
    229     assert_eq!(
    230         RadrootsReticulumProfile::new(
    231             "transport reticulum",
    232             RadrootsReticulumEndpoint::default(),
    233             scope(),
    234             None,
    235             RadrootsReticulumBehavior::RejectDeliveryAttempts,
    236         )
    237         .unwrap_err(),
    238         RadrootsReticulumError::InvalidProfileId
    239     );
    240 
    241     let agent =
    242         RadrootsReticulumAgentEndpoint::parse("reticulum-agent:local").expect("agent endpoint");
    243     let custom = RadrootsReticulumProfile::new(
    244         "transport.reticulum.custom",
    245         RadrootsReticulumEndpoint::default(),
    246         TargetScope::parse("farm.mesh").expect("scope"),
    247         Some(agent.clone()),
    248         RadrootsReticulumBehavior::DeferDeliveryPlans,
    249     )
    250     .expect("profile");
    251     assert_eq!(custom.profile_id(), "transport.reticulum.custom");
    252     assert_eq!(custom.agent_endpoint(), Some(&agent));
    253     assert_eq!(
    254         custom.behavior(),
    255         RadrootsReticulumBehavior::DeferDeliveryPlans
    256     );
    257 
    258     let updated = default
    259         .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans)
    260         .with_agent_endpoint(agent.clone());
    261     assert_eq!(updated.agent_endpoint(), Some(&agent));
    262     assert_eq!(
    263         updated.behavior(),
    264         RadrootsReticulumBehavior::DeferDeliveryPlans
    265     );
    266     assert_eq!(
    267         RadrootsReticulumBehavior::default(),
    268         RadrootsReticulumBehavior::RejectDeliveryAttempts
    269     );
    270 }
    271 
    272 #[test]
    273 fn canonical_source_and_sink_return_request_bound_unavailable_evidence() {
    274     let transport = RadrootsReticulumTransport::default();
    275     assert_eq!(
    276         transport.profile().profile_id(),
    277         "transport.reticulum.default"
    278     );
    279     let transport = RadrootsReticulumTransport::new(transport.profile().clone());
    280 
    281     let sink_status = block_on(EventSink::status(&transport)).expect("sink status");
    282     assert_eq!(sink_status.transport_id(), TransportId::RETICULUM);
    283     assert_eq!(sink_status.maturity(), Maturity::Preview);
    284     assert_eq!(sink_status.availability(), Availability::Unavailable);
    285     assert!(!sink_status.capabilities().can_deliver());
    286     assert_eq!(
    287         sink_status.message(),
    288         RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
    289     );
    290 
    291     let source_status = block_on(EventSource::status(&transport)).expect("source status");
    292     assert_eq!(source_status.transport_id(), TransportId::RETICULUM);
    293     assert_eq!(source_status.maturity(), Maturity::Preview);
    294     assert_eq!(source_status.availability(), Availability::Unavailable);
    295     assert!(!source_status.capabilities().can_fetch());
    296     assert_eq!(
    297         source_status.message(),
    298         RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
    299     );
    300 
    301     let delivery = delivery(target());
    302     let receipt =
    303         block_on(EventSink::deliver(&transport, delivery.clone())).expect("delivery receipt");
    304     receipt
    305         .validate_for_request(&delivery)
    306         .expect("request bound");
    307     assert_eq!(receipt.target_receipts().len(), 1);
    308     assert!(!receipt.target_receipts()[0].was_attempted());
    309     assert_eq!(
    310         receipt.target_receipts()[0].outcome().kind(),
    311         DeliveryOutcomeKind::Unavailable
    312     );
    313     assert_eq!(
    314         receipt.target_receipts()[0].outcome().code(),
    315         Some("transport_unavailable")
    316     );
    317 
    318     let fetch = fetch(target());
    319     let page = block_on(EventSource::fetch(&transport, fetch.clone())).expect("fetch page");
    320     page.validate_for_request(&fetch).expect("request bound");
    321     assert!(page.events().is_empty());
    322     assert_eq!(page.target_outcomes().len(), 1);
    323     assert_eq!(
    324         page.target_outcomes()[0].state(),
    325         FetchTargetState::Unavailable
    326     );
    327     assert_eq!(
    328         page.target_outcomes()[0].message(),
    329         Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE)
    330     );
    331 }
    332 
    333 #[test]
    334 fn source_and_sink_reject_noncanonical_targets_before_adapter_effects() {
    335     let transport = RadrootsReticulumTransport::default();
    336     let invalid_targets = [
    337         Target::nostr_relay("wss://relay.example").expect("Nostr target"),
    338         Target::new_with_metadata(
    339             TransportId::RETICULUM,
    340             "reticulum:remote",
    341             Some(scope()),
    342             None,
    343         )
    344         .expect("wrong URI target"),
    345         Target::new(TransportId::RETICULUM, RADROOTS_RETICULUM_ENDPOINT_URI)
    346             .expect("unscoped target"),
    347     ];
    348 
    349     for invalid in invalid_targets {
    350         let failure = block_on(EventSink::deliver(&transport, delivery(invalid.clone())))
    351             .expect_err("sink must reject target");
    352         assert_eq!(failure.code(), "invalid_transport_contract");
    353         assert_eq!(
    354             block_on(EventSource::fetch(&transport, fetch(invalid))).unwrap_err(),
    355             TransportError::InvalidTargetUri
    356         );
    357     }
    358 }
    359 
    360 #[test]
    361 fn reticulum_error_messages_are_stable() {
    362     let cases = [
    363         (
    364             RadrootsReticulumError::InvalidEndpoint,
    365             "invalid Reticulum endpoint",
    366         ),
    367         (
    368             RadrootsReticulumError::InvalidAgentEndpoint,
    369             "invalid Reticulum agent endpoint",
    370         ),
    371         (
    372             RadrootsReticulumError::InvalidProfileId,
    373             "invalid Reticulum profile id",
    374         ),
    375         (
    376             RadrootsReticulumError::NonReticulumTarget,
    377             "Reticulum transport received a non-Reticulum target",
    378         ),
    379     ];
    380     for (error, expected) in cases {
    381         assert_eq!(error.to_string(), expected);
    382     }
    383 }
    384 
    385 #[test]
    386 #[cfg(feature = "serde")]
    387 fn public_models_round_trip_and_destination_identity_is_revalidated() {
    388     let profile = RadrootsReticulumProfile::default()
    389         .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans);
    390     let encoded = serde_json::to_string(&profile).expect("profile JSON");
    391     assert_eq!(
    392         serde_json::from_str::<RadrootsReticulumProfile>(&encoded).expect("profile round trip"),
    393         profile
    394     );
    395 
    396     let destination = ReticulumDestinationV1::new(
    397         RADROOTS_RETICULUM_ENDPOINT_URI,
    398         scope(),
    399         Some(TargetLabel::parse("Local node").expect("label")),
    400     )
    401     .expect("destination");
    402     let value = serde_json::to_value(&destination).expect("destination JSON");
    403     assert_eq!(
    404         serde_json::from_value::<ReticulumDestinationV1>(value.clone())
    405             .expect("destination round trip"),
    406         destination
    407     );
    408 
    409     for (path, forged) in [
    410         ("uri", serde_json::json!("reticulum:remote")),
    411         ("label", serde_json::json!(" Other node ")),
    412         ("fingerprint", serde_json::json!("0".repeat(64))),
    413     ] {
    414         let mut invalid = value.clone();
    415         invalid[path] = forged;
    416         assert!(
    417             serde_json::from_value::<ReticulumDestinationV1>(invalid).is_err(),
    418             "forged {path} must fail"
    419         );
    420     }
    421 
    422     let mut routing = value;
    423     routing["routing"]["scope"] = serde_json::json!("remote");
    424     assert!(serde_json::from_value::<ReticulumDestinationV1>(routing).is_err());
    425 }