lib

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

commit de4fccb0203c665e2f2caf5e3c852956c5ecfa15
parent 1fc418f66d354eea57ad45b1e6f935f6756aa975
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 15:08:18 +0000

transport: harden Reticulum V1 contract

Add explicit ReticulumDestinationV1, capability, payload, gateway, privacy, and fragmentation contract types; expose them through Reticulum status without reporting fake delivery success; align publish protocol operation capabilities; and add Reticulum outbox idempotency coverage.

Diffstat:
Mcrates/outbox/src/store.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/transport/src/lib.rs | 7+++++++
Acrates/transport/src/reticulum.rs | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/transport/src/status.rs | 40++++++++++++++++++++++++++++++++++++++++
Mcrates/transport/tests/source_boundary.rs | 6++++++
Mcrates/transport/tests/transport.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/transport_publish_protocol/src/lib.rs | 15+++++++++++++++
Mcrates/transport_reticulum/src/lib.rs | 42+++++++++++++++++++++++++++++++++++-------
Mcrates/transport_reticulum/tests/reticulum.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 517 insertions(+), 7 deletions(-)

diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -4548,6 +4548,75 @@ mod tests { } #[tokio::test] + async fn repeated_reticulum_signed_delivery_reuses_one_logical_operation_and_plan() { + let outbox = RadrootsOutbox::open_memory().await.expect("open"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum repeated"); + let signed_event = + radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); + let first = outbox + .enqueue_signed_operation( + RadrootsOutboxSignedOperationInput::new( + "publish_post", + draft.clone(), + signed_event.clone(), + RadrootsOutboxDeliveryPlanInput::new( + "transport.reticulum.default", + 1, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![reticulum_target("reticulum:local")], + ), + true, + 1_007, + 1_000, + ) + .with_idempotency_key("idem-reticulum-repeated"), + ) + .await + .expect("first enqueue"); + let second = outbox + .enqueue_signed_operation( + RadrootsOutboxSignedOperationInput::new( + "publish_post", + draft, + signed_event, + RadrootsOutboxDeliveryPlanInput::new( + "transport.reticulum.default", + 1, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![reticulum_target("reticulum:local")], + ), + true, + 1_017, + 1_010, + ) + .with_idempotency_key("idem-reticulum-repeated"), + ) + .await + .expect("second enqueue"); + + assert_eq!(first.status, RadrootsOutboxEnqueueStatus::Inserted); + assert_eq!(second.status, RadrootsOutboxEnqueueStatus::Existing); + assert_eq!(first.operation_id, second.operation_id); + assert_eq!(first.outbox_event_id, second.outbox_event_id); + assert_eq!(first.delivery_plan_id, second.delivery_plan_id); + assert_eq!( + first.operation_idempotency_digest, + second.operation_idempotency_digest + ); + assert_eq!( + first.delivery_plan_idempotency_digest, + second.delivery_plan_idempotency_digest + ); + assert_eq!(table_count(&outbox, "outbox_operations").await, 1); + assert_eq!(table_count(&outbox, "outbox_event").await, 1); + assert_eq!(table_count(&outbox, "outbox_delivery_plan").await, 1); + assert_eq!(table_count(&outbox, "outbox_delivery_target").await, 1); + let records = outbox.reticulum_events(None, 10).await.expect("records"); + assert_eq!(records.len(), 1); + assert_eq!(records[0].event.outbox_event_id, first.outbox_event_id); + } + + #[tokio::test] async fn reticulum_events_report_deferred_records() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let reject_draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum record"); diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs @@ -8,6 +8,7 @@ mod error; mod kind; mod message; mod payload; +mod reticulum; mod status; mod target; mod transport; @@ -27,6 +28,12 @@ pub use message::{ RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, }; pub use payload::RadrootsTransportPayload; +pub use reticulum::{ + ReticulumCapabilityReportV1, ReticulumDestinationV1, ReticulumDuplicateFragmentBehaviorV1, + ReticulumFragmentIntegrityV1, ReticulumFragmentPolicyV1, ReticulumFragmentationModeV1, + ReticulumGatewaySemanticsV1, ReticulumPayloadPolicyV1, ReticulumPrivacySemanticsV1, + ReticulumRoutingMetadataV1, +}; pub use status::{ RadrootsTransportCapabilities, RadrootsTransportDeliveryTargetStatus, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportStatus, diff --git a/crates/transport/src/reticulum.rs b/crates/transport/src/reticulum.rs @@ -0,0 +1,194 @@ +use crate::{ + RADROOTS_RETICULUM_ENDPOINT_URI, RadrootsTransportError, RadrootsTransportKind, + RadrootsTransportMeshScopeId, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, + RadrootsTransportTargetLabel, RadrootsTransportTargetUri, +}; + +pub const RETICULUM_V1_MAX_PAYLOAD_BYTES: usize = 64 * 1024; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReticulumFragmentationModeV1 { + Unsupported, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReticulumDuplicateFragmentBehaviorV1 { + Reject, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReticulumFragmentIntegrityV1 { + PayloadDigest, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReticulumFragmentPolicyV1 { + pub mode: ReticulumFragmentationModeV1, + pub max_fragment_count: u16, + pub max_reassembled_bytes: usize, + pub duplicate_fragment_behavior: ReticulumDuplicateFragmentBehaviorV1, + pub integrity_verification: ReticulumFragmentIntegrityV1, +} + +impl ReticulumFragmentPolicyV1 { + pub const fn unsupported() -> Self { + Self { + mode: ReticulumFragmentationModeV1::Unsupported, + max_fragment_count: 1, + max_reassembled_bytes: RETICULUM_V1_MAX_PAYLOAD_BYTES, + duplicate_fragment_behavior: ReticulumDuplicateFragmentBehaviorV1::Reject, + integrity_verification: ReticulumFragmentIntegrityV1::PayloadDigest, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReticulumPayloadPolicyV1 { + pub max_payload_bytes: usize, + pub fragment_policy: ReticulumFragmentPolicyV1, +} + +impl ReticulumPayloadPolicyV1 { + pub const fn v1() -> Self { + Self { + max_payload_bytes: RETICULUM_V1_MAX_PAYLOAD_BYTES, + fragment_policy: ReticulumFragmentPolicyV1::unsupported(), + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReticulumGatewaySemanticsV1 { + NoGatewayForwarding, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReticulumPrivacySemanticsV1 { + CanonicalSignedEventBytesOnly, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReticulumRoutingMetadataV1 { + pub scope: RadrootsTransportMeshScopeId, + pub gateway: ReticulumGatewaySemanticsV1, + pub privacy: ReticulumPrivacySemanticsV1, +} + +impl ReticulumRoutingMetadataV1 { + pub fn local() -> Self { + Self { + scope: RadrootsTransportMeshScopeId::local_reticulum(), + gateway: ReticulumGatewaySemanticsV1::NoGatewayForwarding, + privacy: ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReticulumDestinationV1 { + pub uri: RadrootsTransportTargetUri, + pub routing: ReticulumRoutingMetadataV1, + pub label: Option<RadrootsTransportTargetLabel>, + pub fingerprint: RadrootsTransportTargetFingerprint, +} + +impl ReticulumDestinationV1 { + pub fn local() -> Self { + Self::new( + RADROOTS_RETICULUM_ENDPOINT_URI, + ReticulumRoutingMetadataV1::local().scope, + None, + ) + .expect("local Reticulum destination") + } + + pub fn new( + uri: impl AsRef<str>, + scope: RadrootsTransportMeshScopeId, + label: Option<RadrootsTransportTargetLabel>, + ) -> Result<Self, RadrootsTransportError> { + let target = RadrootsTransportTarget::reticulum_with_metadata( + uri.as_ref(), + Some(scope), + label.clone(), + )?; + Ok(Self { + uri: target.uri, + routing: ReticulumRoutingMetadataV1 { + scope: target.scope.expect("Reticulum destination scope"), + gateway: ReticulumGatewaySemanticsV1::NoGatewayForwarding, + privacy: ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly, + }, + label, + fingerprint: target.fingerprint, + }) + } + + pub fn from_target(target: &RadrootsTransportTarget) -> Result<Self, RadrootsTransportError> { + if target.kind != RadrootsTransportKind::Reticulum + || target.uri.as_str() != RADROOTS_RETICULUM_ENDPOINT_URI + { + return Err(RadrootsTransportError::InvalidTargetUri); + } + let Some(scope) = target.scope.clone() else { + return Err(RadrootsTransportError::EmptyTargetScope); + }; + Ok(Self { + uri: target.uri.clone(), + routing: ReticulumRoutingMetadataV1 { + scope, + gateway: ReticulumGatewaySemanticsV1::NoGatewayForwarding, + privacy: ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly, + }, + label: target.label.clone(), + fingerprint: target.fingerprint.clone(), + }) + } + + pub fn transport_target(&self) -> Result<RadrootsTransportTarget, RadrootsTransportError> { + RadrootsTransportTarget::reticulum_with_metadata( + self.uri.as_str(), + Some(self.routing.scope.clone()), + self.label.clone(), + ) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReticulumCapabilityReportV1 { + pub delivery_required: bool, + pub fetch_required: bool, + pub can_deliver: bool, + pub can_fetch: bool, + pub can_discover: bool, + pub can_forward_gateway: bool, + pub can_observe_receipts: bool, + pub destination: ReticulumDestinationV1, + pub payload_policy: ReticulumPayloadPolicyV1, +} + +impl ReticulumCapabilityReportV1 { + pub fn unavailable_local() -> Self { + Self { + delivery_required: true, + fetch_required: false, + can_deliver: false, + can_fetch: false, + can_discover: false, + can_forward_gateway: false, + can_observe_receipts: false, + destination: ReticulumDestinationV1::local(), + payload_policy: ReticulumPayloadPolicyV1::v1(), + } + } +} diff --git a/crates/transport/src/status.rs b/crates/transport/src/status.rs @@ -180,6 +180,9 @@ impl RadrootsTransportOutcome { pub struct RadrootsTransportCapabilities { pub deliver: bool, pub fetch: bool, + pub discovery: bool, + pub gateway_forwarding: bool, + pub receipt_observation: bool, } impl RadrootsTransportCapabilities { @@ -187,6 +190,9 @@ impl RadrootsTransportCapabilities { Self { deliver: false, fetch: false, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, } } @@ -194,6 +200,9 @@ impl RadrootsTransportCapabilities { Self { deliver: true, fetch: false, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, } } @@ -201,6 +210,9 @@ impl RadrootsTransportCapabilities { Self { deliver: false, fetch: true, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, } } @@ -208,6 +220,34 @@ impl RadrootsTransportCapabilities { Self { deliver: true, fetch: true, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, + } + } + + pub const fn with_discovery(mut self, discovery: bool) -> Self { + self.discovery = discovery; + self + } + + pub const fn with_gateway_forwarding(mut self, gateway_forwarding: bool) -> Self { + self.gateway_forwarding = gateway_forwarding; + self + } + + pub const fn with_receipt_observation(mut self, receipt_observation: bool) -> Self { + self.receipt_observation = receipt_observation; + self + } + + pub const fn reticulum_unavailable() -> Self { + Self { + deliver: false, + fetch: false, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, } } } diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs @@ -551,6 +551,9 @@ fn transport_publish_capabilities_keep_canonical_status_fields() { "pub struct TransportPublishOperationCapabilities", "pub deliver: bool,", "pub fetch: bool,", + "pub discovery: bool,", + "pub gateway_forwarding: bool,", + "pub receipt_observation: bool,", "TransportPublishImplementation::Real", "TransportPublishCapabilityMaturity::Preview", "TransportPublishCapabilityAvailability::Unavailable", @@ -560,6 +563,9 @@ fn transport_publish_capabilities_keep_canonical_status_fields() { "capabilities: TransportPublishOperationCapabilities", "deliver: true", "fetch: false", + "discovery: false", + "gateway_forwarding: false", + "receipt_observation: false", ] { assert!( source.contains(required), diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs @@ -9,6 +9,9 @@ use radroots_transport::{ RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, RadrootsTransportTargetUri, + ReticulumCapabilityReportV1, ReticulumDuplicateFragmentBehaviorV1, + ReticulumFragmentIntegrityV1, ReticulumFragmentationModeV1, ReticulumGatewaySemanticsV1, + ReticulumPrivacySemanticsV1, }; use serde_json::Value; @@ -40,6 +43,82 @@ fn target_fingerprints_are_stable_and_transport_scoped() { } #[test] +fn reticulum_destination_v1_is_canonical_and_stable() { + let target = RadrootsTransportTarget::reticulum().expect("reticulum target"); + let destination = radroots_transport::ReticulumDestinationV1::from_target(&target) + .expect("destination from target"); + let local = radroots_transport::ReticulumDestinationV1::local(); + + assert_eq!(destination, local); + assert_eq!(destination.uri.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!( + destination.routing.scope.as_str(), + RADROOTS_RETICULUM_SCOPE_ID + ); + assert_eq!( + destination.routing.gateway, + ReticulumGatewaySemanticsV1::NoGatewayForwarding + ); + assert_eq!( + destination.routing.privacy, + ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly + ); + assert_eq!(destination.fingerprint, target.fingerprint); + assert_eq!( + destination + .transport_target() + .expect("transport target") + .fingerprint, + target.fingerprint + ); + assert_eq!( + destination.fingerprint.as_str(), + "39142c9a79d6912655e0ad00fb5dbfbe9d2d91b4999e5d68d04a81d89a77f831" + ); + assert!( + radroots_transport::ReticulumDestinationV1::new( + "reticulum:other", + RadrootsTransportMeshScopeId::local_reticulum(), + None, + ) + .is_err() + ); +} + +#[test] +fn reticulum_capability_report_v1_is_explicitly_unavailable_without_fragmentation() { + let report = ReticulumCapabilityReportV1::unavailable_local(); + + assert!(report.delivery_required); + assert!(!report.fetch_required); + assert!(!report.can_deliver); + assert!(!report.can_fetch); + assert!(!report.can_discover); + assert!(!report.can_forward_gateway); + assert!(!report.can_observe_receipts); + assert_eq!( + report.payload_policy.fragment_policy.mode, + ReticulumFragmentationModeV1::Unsupported + ); + assert_eq!(report.payload_policy.fragment_policy.max_fragment_count, 1); + assert_eq!( + report.payload_policy.fragment_policy.max_reassembled_bytes, + report.payload_policy.max_payload_bytes + ); + assert_eq!( + report + .payload_policy + .fragment_policy + .duplicate_fragment_behavior, + ReticulumDuplicateFragmentBehaviorV1::Reject + ); + assert_eq!( + report.payload_policy.fragment_policy.integrity_verification, + ReticulumFragmentIntegrityV1::PayloadDigest + ); +} + +#[test] fn transport_kind_parser_round_trips_first_wave_canonical_labels() { assert_eq!( RadrootsTransportKind::parse(" NOSTR ").expect("nostr kind"), @@ -298,6 +377,9 @@ fn transport_status_models_canonical_configuration_and_delivery_usability() { assert_eq!(json["usable_for_delivery"], true); assert_eq!(json["capabilities"]["deliver"], true); assert_eq!(json["capabilities"]["fetch"], false); + assert_eq!(json["capabilities"]["discovery"], false); + assert_eq!(json["capabilities"]["gateway_forwarding"], false); + assert_eq!(json["capabilities"]["receipt_observation"], false); assert_eq!(json["message"], "ready"); for retired in [ "kind", diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs @@ -949,6 +949,9 @@ impl TransportPublishCapabilities { capabilities: TransportPublishOperationCapabilities { deliver: true, fetch: false, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, }, reticulum_behavior: None, message: "Nostr relay publish is available".to_owned(), @@ -963,6 +966,9 @@ impl TransportPublishCapabilities { capabilities: TransportPublishOperationCapabilities { deliver: false, fetch: false, + discovery: false, + gateway_forwarding: false, + receipt_observation: false, }, reticulum_behavior: Some( TransportPublishReticulumBehavior::RejectDeliveryAttempts, @@ -1045,6 +1051,9 @@ pub struct TransportPublishTransportCapability { pub struct TransportPublishOperationCapabilities { pub deliver: bool, pub fetch: bool, + pub discovery: bool, + pub gateway_forwarding: bool, + pub receipt_observation: bool, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -1543,6 +1552,9 @@ mod tests { assert!(nostr.usable_for_delivery); assert!(nostr.capabilities.deliver); assert!(!nostr.capabilities.fetch); + assert!(!nostr.capabilities.discovery); + assert!(!nostr.capabilities.gateway_forwarding); + assert!(!nostr.capabilities.receipt_observation); let reticulum = capabilities .publish .transports @@ -1565,6 +1577,9 @@ mod tests { assert!(!reticulum.usable_for_delivery); assert!(!reticulum.capabilities.deliver); assert!(!reticulum.capabilities.fetch); + assert!(!reticulum.capabilities.discovery); + assert!(!reticulum.capabilities.gateway_forwarding); + assert!(!reticulum.capabilities.receipt_observation); assert_eq!( reticulum.reticulum_behavior, Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts) diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -10,13 +10,14 @@ use alloc::vec::Vec; use core::fmt; use radroots_transport::{ RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransport, - RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, - RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, - RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, - RadrootsTransportOutcomeKind, RadrootsTransportStatus, RadrootsTransportTarget, - RadrootsTransportTargetReceipt, + RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, + RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, + RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, + RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, + RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, + RadrootsTransportMeshScopeId, RadrootsTransportOutcome, RadrootsTransportOutcomeKind, + RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt, + ReticulumCapabilityReportV1, ReticulumDestinationV1, ReticulumPayloadPolicyV1, }; const DEFAULT_PROFILE_ID: &str = "transport.reticulum.default"; @@ -118,6 +119,8 @@ pub struct RadrootsReticulumProfile { scope: RadrootsTransportMeshScopeId, agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, behavior: RadrootsReticulumBehavior, + destination: ReticulumDestinationV1, + capability_report: ReticulumCapabilityReportV1, } impl RadrootsReticulumProfile { @@ -132,22 +135,34 @@ impl RadrootsReticulumProfile { if profile_id.trim().is_empty() || profile_id.chars().any(char::is_whitespace) { return Err(RadrootsReticulumError::InvalidProfileId); } + let destination = ReticulumDestinationV1::new(endpoint.as_str(), scope.clone(), None) + .map_err(|_| RadrootsReticulumError::InvalidEndpoint)?; + let capability_report = ReticulumCapabilityReportV1 { + destination: destination.clone(), + payload_policy: ReticulumPayloadPolicyV1::v1(), + ..ReticulumCapabilityReportV1::unavailable_local() + }; Ok(Self { profile_id, endpoint, scope, agent_endpoint, behavior, + destination, + capability_report, }) } pub fn deferred_until_implemented() -> Self { + let capability_report = ReticulumCapabilityReportV1::unavailable_local(); Self { profile_id: DEFAULT_PROFILE_ID.to_owned(), endpoint: RadrootsReticulumEndpoint::default(), scope: RadrootsTransportMeshScopeId::local_reticulum(), agent_endpoint: None, behavior: RadrootsReticulumBehavior::RejectDeliveryAttempts, + destination: capability_report.destination.clone(), + capability_report, } } @@ -181,11 +196,21 @@ impl RadrootsReticulumProfile { self.behavior } + pub fn destination(&self) -> &ReticulumDestinationV1 { + &self.destination + } + + pub fn capability_report(&self) -> &ReticulumCapabilityReportV1 { + &self.capability_report + } + pub fn status(&self) -> RadrootsReticulumStatus { RadrootsReticulumStatus { behavior: self.behavior, scope: self.scope.clone(), agent_endpoint: self.agent_endpoint.clone(), + destination: self.destination.clone(), + capability_report: self.capability_report.clone(), transport_status: RadrootsTransportStatus::new( RadrootsTransportKind::Reticulum, true, @@ -195,6 +220,7 @@ impl RadrootsReticulumProfile { ) .with_maturity(RadrootsTransportCapabilityMaturity::Preview) .with_availability(RadrootsTransportCapabilityAvailability::Unavailable) + .with_capabilities(RadrootsTransportCapabilities::reticulum_unavailable()) .with_profile_id(self.profile_id.clone()) .with_endpoint_uri(self.endpoint.as_str()), } @@ -213,6 +239,8 @@ pub struct RadrootsReticulumStatus { pub behavior: RadrootsReticulumBehavior, pub scope: RadrootsTransportMeshScopeId, pub agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, + pub destination: ReticulumDestinationV1, + pub capability_report: ReticulumCapabilityReportV1, pub transport_status: RadrootsTransportStatus, } diff --git a/crates/transport_reticulum/tests/reticulum.rs b/crates/transport_reticulum/tests/reticulum.rs @@ -6,6 +6,8 @@ use radroots_transport::{ RadrootsTransportFetchRequest, RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportPayload, RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + ReticulumDuplicateFragmentBehaviorV1, ReticulumFragmentIntegrityV1, + ReticulumFragmentationModeV1, ReticulumGatewaySemanticsV1, ReticulumPrivacySemanticsV1, }; use radroots_transport_reticulum::{ RadrootsReticulumAgentEndpoint, RadrootsReticulumBehavior, RadrootsReticulumEndpoint, @@ -79,6 +81,61 @@ fn default_profile_is_configured_deferred_until_implemented_and_rejecting() { Some(RADROOTS_RETICULUM_ENDPOINT_URI) ); assert_eq!( + profile.destination().uri.as_str(), + RADROOTS_RETICULUM_ENDPOINT_URI + ); + assert_eq!( + profile.destination().routing.scope.as_str(), + RADROOTS_RETICULUM_SCOPE_ID + ); + assert_eq!( + status.destination.routing.gateway, + ReticulumGatewaySemanticsV1::NoGatewayForwarding + ); + assert_eq!( + status.destination.routing.privacy, + ReticulumPrivacySemanticsV1::CanonicalSignedEventBytesOnly + ); + assert!(status.capability_report.delivery_required); + assert!(!status.capability_report.fetch_required); + assert!(!status.capability_report.can_deliver); + assert!(!status.capability_report.can_fetch); + assert!(!status.capability_report.can_discover); + assert!(!status.capability_report.can_forward_gateway); + assert!(!status.capability_report.can_observe_receipts); + assert_eq!( + status.capability_report.destination.fingerprint, + status.destination.fingerprint + ); + assert_eq!( + status.capability_report.payload_policy.fragment_policy.mode, + ReticulumFragmentationModeV1::Unsupported + ); + assert_eq!( + status + .capability_report + .payload_policy + .fragment_policy + .max_fragment_count, + 1 + ); + assert_eq!( + status + .capability_report + .payload_policy + .fragment_policy + .duplicate_fragment_behavior, + ReticulumDuplicateFragmentBehaviorV1::Reject + ); + assert_eq!( + status + .capability_report + .payload_policy + .fragment_policy + .integrity_verification, + ReticulumFragmentIntegrityV1::PayloadDigest + ); + assert_eq!( status.transport_status.message, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE ); @@ -215,6 +272,15 @@ fn endpoint_and_profile_validation_are_strict_and_canonical() { profile.agent_endpoint().map(|endpoint| endpoint.as_str()), Some("reticulum-agent://localhost:19999") ); + assert_eq!( + profile + .capability_report() + .destination + .routing + .scope + .as_str(), + RADROOTS_RETICULUM_SCOPE_ID + ); } #[test] @@ -497,6 +563,9 @@ fn configured_agent_endpoint_is_metadata_only_for_status_delivery_and_fetch() { assert!(!status.transport_status.usable_for_delivery); assert!(!status.transport_status.capabilities.deliver); assert!(!status.transport_status.capabilities.fetch); + assert!(!status.transport_status.capabilities.discovery); + assert!(!status.transport_status.capabilities.gateway_forwarding); + assert!(!status.transport_status.capabilities.receipt_observation); let receipt = transport .deliver(delivery_request(vec![reticulum_target(