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 }