commit 2f88cbabf924d45f48afda26bdc5625938bc480f
parent a83f1388a9f6b5a8538d87f8fcbc3dbd1bacf9d3
Author: triesap <tyson@radroots.org>
Date: Sun, 9 Aug 2026 22:52:47 +0000
transport_nostr: authorize canonical relay publication
Diffstat:
5 files changed, 61 insertions(+), 41 deletions(-)
diff --git a/crates/mobile_core/src/runtime/builder.rs b/crates/mobile_core/src/runtime/builder.rs
@@ -124,9 +124,9 @@ mod tests {
assert_eq!(report.state, "configured");
assert_eq!(report.relays.len(), 1);
assert_eq!(report.relays[0].relay_url, "wss://radroots.org");
- assert_eq!(report.relays[0].access, "read_only");
+ assert_eq!(report.relays[0].access, "read_write");
assert_eq!(report.relays[0].read_state, "unobserved");
- assert_eq!(report.relays[0].write_state, "unsupported");
+ assert_eq!(report.relays[0].write_state, "unobserved");
}
runtime.shutdown().await.expect("shutdown");
}
diff --git a/crates/mobile_ffi/tests/runtime_delegation.rs b/crates/mobile_ffi/tests/runtime_delegation.rs
@@ -39,9 +39,9 @@ async fn native_boundary_delegates_the_complete_core_surface() {
assert_eq!(public.read_availability, "unavailable");
assert_eq!(public.write_availability, "unavailable");
assert_eq!(public.relays.len(), 1);
- assert_eq!(public.relays[0].access, "read_only");
+ assert_eq!(public.relays[0].access, "read_write");
assert_eq!(public.relays[0].read_state, "unobserved");
- assert_eq!(public.relays[0].write_state, "unsupported");
+ assert_eq!(public.relays[0].write_state, "unobserved");
runtime
.configure_public_relays(vec!["wss://write.example".to_owned()])
diff --git a/crates/transport_nostr/src/client.rs b/crates/transport_nostr/src/client.rs
@@ -325,7 +325,7 @@ mod tests {
.expect("connections");
assert_eq!(config.relays().len(), 3);
assert_eq!(config.read_relays().count(), 3);
- assert_eq!(config.write_relays().count(), 2);
+ assert_eq!(config.write_relays().count(), 3);
assert_eq!(config.profile_kind(), RelayProfileKind::Public);
assert_eq!(config.connect_timeout_ms(), 1);
assert_eq!(config.request_timeout_ms(), 2);
diff --git a/crates/transport_nostr/src/profile.rs b/crates/transport_nostr/src/profile.rs
@@ -3,7 +3,7 @@
use crate::{Error, RelayUrl, RelayUrlPolicy};
use std::collections::BTreeSet;
-/// Bundled public relay used for read discovery only.
+/// Bundled canonical Radroots relay used for reads and publication.
pub const DEFAULT_PUBLIC_RELAY: &str = "wss://radroots.org";
/// Directional access authorized for one configured relay.
@@ -34,7 +34,7 @@ impl RelayAccess {
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum RelayProfileKind {
- /// Public-Internet profile with the bundled read-only relay.
+ /// Public-Internet profile with the bundled canonical relay.
Public,
/// Development-only profile restricted to exact loopback destinations.
Simulator,
@@ -92,10 +92,9 @@ pub struct RelayProfile {
impl RelayProfile {
/// Builds the ordinary public profile.
///
- /// `wss://radroots.org/` is always present as read-only. Every supplied
+ /// `wss://radroots.org/` is always present as read-write. Every additional
/// writable relay must be a TLS public-Internet destination. Supplying the
- /// bundled relay as writable is rejected rather than silently broadening
- /// its authority.
+ /// bundled relay again is rejected as a duplicate.
pub fn public<I, S>(writable_relays: I) -> Result<Self, Error>
where
I: IntoIterator<Item = S>,
@@ -104,7 +103,7 @@ impl RelayProfile {
let mut endpoints = vec![RelayEndpoint::new(
DEFAULT_PUBLIC_RELAY,
RelayUrlPolicy::Public,
- RelayAccess::ReadOnly,
+ RelayAccess::ReadWrite,
)?];
endpoints.extend(parse_endpoints(
writable_relays,
@@ -133,7 +132,7 @@ impl RelayProfile {
/// Builds a physical-device profile from explicit writable TLS endpoints.
///
- /// The bundled public relay remains read-only. Writable endpoints may
+ /// The bundled canonical relay remains read-write. Additional endpoints may
/// resolve to public or private addresses, but loopback, unspecified, and
/// multicast destinations remain forbidden before and after resolution.
pub fn device<I, S>(writable_relays: I) -> Result<Self, Error>
@@ -144,7 +143,7 @@ impl RelayProfile {
let mut endpoints = vec![RelayEndpoint::new(
DEFAULT_PUBLIC_RELAY,
RelayUrlPolicy::Public,
- RelayAccess::ReadOnly,
+ RelayAccess::ReadWrite,
)?];
endpoints.extend(parse_endpoints(
writable_relays,
@@ -208,12 +207,12 @@ mod tests {
use super::*;
#[test]
- fn public_profile_never_promotes_the_bundled_relay_to_writable() {
+ fn public_profile_authorizes_the_canonical_relay_for_publication() {
let profile = RelayProfile::public(["wss://write.example"]).expect("public profile");
assert_eq!(profile.kind(), RelayProfileKind::Public);
assert_eq!(profile.endpoints().len(), 2);
assert_eq!(profile.endpoints()[0].url().as_str(), DEFAULT_PUBLIC_RELAY);
- assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadOnly);
+ assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite);
assert_eq!(profile.endpoints()[1].access(), RelayAccess::ReadWrite);
assert!(RelayProfile::public([DEFAULT_PUBLIC_RELAY]).is_err());
assert!(RelayProfile::public(["ws://public.example"]).is_err());
@@ -232,6 +231,7 @@ mod tests {
fn device_profile_requires_tls_and_rejects_duplicate_authority() {
let profile = RelayProfile::device(["wss://10.0.0.5:7447"]).expect("device profile");
assert_eq!(profile.kind(), RelayProfileKind::Device);
+ assert_eq!(profile.endpoints()[0].access(), RelayAccess::ReadWrite);
assert_eq!(
profile.endpoints()[1].policy(),
RelayUrlPolicy::PrivateNetwork
diff --git a/crates/transport_nostr/src/status.rs b/crates/transport_nostr/src/status.rs
@@ -644,9 +644,9 @@ mod tests {
let config =
Config::from_profile(RelayProfile::public(["wss://write.example"]).expect("profile"))
.with_reconnect_backoff(ReconnectBackoff::new(10, 40).expect("backoff"));
- let read_only = config.relays()[0].clone();
+ let canonical = config.relays()[0].clone();
let writable = config.relays()[1].clone();
- (StatusTracker::new(&config), read_only, writable)
+ (StatusTracker::new(&config), canonical, writable)
}
#[test]
@@ -672,20 +672,20 @@ mod tests {
#[test]
fn status_requires_directional_evidence_and_backoff_is_monotonic() {
- let (tracker, read_only, writable) = tracker();
+ let (tracker, canonical, writable) = tracker();
let initial = tracker.report();
assert_eq!(initial.read_availability(), Availability::Unavailable);
assert_eq!(initial.write_availability(), Availability::Unavailable);
assert_eq!(initial.state(), RelayAggregateState::Configured);
assert_eq!(
initial.relays()[0].write().state(),
- RelayEvidenceState::Unsupported
+ RelayEvidenceState::Unobserved
);
- assert!(!tracker.may_write(&read_only, 100));
+ assert!(tracker.may_write(&canonical, 100));
- tracker.begin_read(&read_only, 100);
+ tracker.begin_read(&canonical, 100);
assert_eq!(tracker.report().state(), RelayAggregateState::Connecting);
- tracker.record_read(&read_only, true, false, 100);
+ tracker.record_read(&canonical, true, false, 100);
tracker.record_read(&writable, false, true, 100);
tracker.record_write(&writable, false, true, 100);
let partial = tracker.report();
@@ -706,14 +706,29 @@ mod tests {
RelayEvidenceState::Unavailable
);
tracker.record_write(&writable, true, false, 130);
- let available = tracker.report();
- assert_eq!(available.write_availability(), Availability::Available);
- assert_eq!(available.relays()[1].write().consecutive_failures(), 0);
+ let partially_available = tracker.report();
assert_eq!(
- available.relays()[1].write().last_success_unix_ms(),
+ partially_available.write_availability(),
+ Availability::Degraded
+ );
+ assert_eq!(
+ partially_available.relays()[1]
+ .write()
+ .consecutive_failures(),
+ 0
+ );
+ assert_eq!(
+ partially_available.relays()[1]
+ .write()
+ .last_success_unix_ms(),
Some(130)
);
assert!(tracker.may_write(&writable, 130));
+ tracker.record_write(&canonical, true, false, 130);
+ assert_eq!(
+ tracker.report().write_availability(),
+ Availability::Available
+ );
assert_eq!(
source_status(&tracker).availability(),
Availability::Degraded
@@ -724,7 +739,9 @@ mod tests {
);
tracker.record_write(&writable, false, false, 140);
+ tracker.record_write(&canonical, false, false, 140);
assert!(!tracker.may_write(&writable, u64::MAX));
+ assert!(!tracker.may_write(&canonical, u64::MAX));
assert_eq!(
tracker.report().relays()[1]
.write()
@@ -762,25 +779,26 @@ mod tests {
}
#[test]
- fn all_success_is_available_and_no_writable_relay_is_honestly_unavailable() {
+ fn canonical_relay_read_and_write_success_is_available() {
let config =
Config::from_profile(RelayProfile::public(Vec::<String>::new()).expect("profile"));
let tracker = StatusTracker::new(&config);
let relay = config.relays()[0].clone();
tracker.record_read(&relay, true, false, 1);
+ tracker.record_write(&relay, true, false, 1);
assert_eq!(
source_status(&tracker).availability(),
Availability::Available
);
let sink = sink_status(&tracker);
- assert!(!sink.is_configured());
- assert_eq!(sink.availability(), Availability::Unavailable);
- assert_eq!(tracker.report().state(), RelayAggregateState::ReadOnly);
+ assert!(sink.is_configured());
+ assert_eq!(sink.availability(), Availability::Available);
+ assert_eq!(tracker.report().state(), RelayAggregateState::Writable);
}
#[test]
fn aggregate_states_and_unconfigured_targets_cover_fail_closed_edges() {
- let (live, read_only, writable) = tracker();
+ let (live, canonical, writable) = tracker();
let unknown =
RelayUrl::parse("wss://unknown.example", crate::RelayUrlPolicy::Public).expect("relay");
@@ -788,16 +806,16 @@ mod tests {
live.begin_write(&unknown, 1);
live.record_read(&unknown, true, false, 1);
live.record_write(&unknown, true, false, 1);
- live.begin_write(&read_only, 1);
- live.record_write(&read_only, true, false, 1);
+ live.begin_write(&canonical, 1);
+ live.record_write(&canonical, true, false, 1);
assert_eq!(
live.report().relays()[0].write().state(),
- RelayEvidenceState::Unsupported
+ RelayEvidenceState::Available
);
- live.begin_read(&read_only, 2);
- live.begin_read(&read_only, 1);
- live.record_read(&read_only, true, false, 2);
+ live.begin_read(&canonical, 2);
+ live.begin_read(&canonical, 1);
+ live.record_read(&canonical, true, false, 2);
live.record_read(&writable, true, false, 2);
live.record_write(&writable, true, false, 2);
let writable_report = live.report();
@@ -808,15 +826,17 @@ mod tests {
Availability::Available
);
- let (offline, read_only, writable) = tracker();
- offline.record_read(&read_only, false, true, 10);
+ let (offline, canonical, writable) = tracker();
+ offline.record_read(&canonical, false, true, 10);
offline.record_read(&writable, false, true, 10);
+ offline.record_write(&canonical, false, true, 10);
offline.record_write(&writable, false, true, 10);
assert_eq!(offline.report().state(), RelayAggregateState::Offline);
- let (failed, read_only, writable) = tracker();
- failed.record_read(&read_only, false, false, 10);
+ let (failed, canonical, writable) = tracker();
+ failed.record_read(&canonical, false, false, 10);
failed.record_read(&writable, false, false, 10);
+ failed.record_write(&canonical, false, false, 10);
failed.record_write(&writable, false, false, 10);
assert_eq!(failed.report().state(), RelayAggregateState::Failed);