Skip to content

Commit 7ca76cf

Browse files
JSKittyclaude
andcommitted
fix: MLS relay resilience and commit-under-lock safety
- Move commit creation (add_members/remove_members) inside the per-group sync lock to prevent race conditions with concurrent sync operations - Fix retry logic: retry all transient relay errors, only bail early on definitive rejections (duplicate/blocked) - Add active_trusted_relays() helper that filters TRUSTED_RELAYS to only those currently in the relay pool — nostr_sdk's send_event_to / fetch_events_from / stream_events_from all fail entirely if any requested relay isn't in the pool, even if others are connected - Update all call sites (mls, keypackages, invites, typing indicators, account) to use the filtered relay list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e742ef0 commit 7ca76cf

9 files changed

Lines changed: 232 additions & 141 deletions

File tree

src-tauri/src/commands/account.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use nostr_sdk::prelude::*;
1111
use tauri::{AppHandle, Emitter, Manager, Runtime};
1212

13-
use crate::{STATE, TAURI_APP, NOSTR_CLIENT, MY_KEYS, MY_PUBLIC_KEY, MNEMONIC_SEED, PENDING_NSEC, PENDING_INVITE, TRUSTED_RELAYS};
13+
use crate::{STATE, TAURI_APP, NOSTR_CLIENT, MY_KEYS, MY_PUBLIC_KEY, MNEMONIC_SEED, PENDING_NSEC, PENDING_INVITE, active_trusted_relays};
1414
use crate::{Profile, account_manager, db, crypto, commands};
1515

1616
// ============================================================================
@@ -319,7 +319,7 @@ pub async fn encrypt(input: String, password: Option<String>) -> String {
319319
match client.sign_event_builder(event_builder).await {
320320
Ok(event) => {
321321
// Send only to trusted relays
322-
match client.send_event_to(TRUSTED_RELAYS.iter().copied(), &event).await {
322+
match client.send_event_to(active_trusted_relays().await.into_iter(), &event).await {
323323
Ok(_) => println!("Successfully broadcast invite acceptance to trusted relays"),
324324
Err(e) => eprintln!("Failed to broadcast invite acceptance: {}", e),
325325
}
@@ -515,7 +515,7 @@ pub async fn setup_encryption<R: Runtime>(
515515
.tag(Tag::public_key(inviter_pubkey));
516516
match client.sign_event_builder(event_builder).await {
517517
Ok(event) => {
518-
match client.send_event_to(TRUSTED_RELAYS.iter().copied(), &event).await {
518+
match client.send_event_to(active_trusted_relays().await.into_iter(), &event).await {
519519
Ok(_) => println!("Successfully broadcast invite acceptance to trusted relays"),
520520
Err(e) => eprintln!("Failed to broadcast invite acceptance: {}", e),
521521
}

src-tauri/src/commands/invites.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use nostr_sdk::prelude::*;
1111
use rand::{thread_rng, Rng};
1212
use rand::distributions::Alphanumeric;
1313

14-
use crate::{TAURI_APP, NOSTR_CLIENT, TRUSTED_RELAYS, PENDING_INVITE, PendingInviteAcceptance};
14+
use crate::{TAURI_APP, NOSTR_CLIENT, active_trusted_relays, PENDING_INVITE, PendingInviteAcceptance};
1515
use crate::db;
1616

1717
// ============================================================================
@@ -95,7 +95,7 @@ pub async fn get_or_create_invite_code() -> Result<String, String> {
9595
let event = client.sign_event_builder(event_builder).await.map_err(|e| e.to_string())?;
9696

9797
// Send only to trusted relays
98-
client.send_event_to(TRUSTED_RELAYS.iter().copied(), &event).await.map_err(|e| e.to_string())?;
98+
client.send_event_to(active_trusted_relays().await.into_iter(), &event).await.map_err(|e| e.to_string())?;
9999

100100
// Store locally
101101
db::set_sql_setting(handle.clone(), "invite_code".to_string(), new_code.clone())
@@ -124,7 +124,7 @@ pub async fn accept_invite_code(invite_code: String) -> Result<String, String> {
124124

125125
// Find the invite event
126126
let mut events = client
127-
.stream_events_from(TRUSTED_RELAYS.to_vec(), filter, std::time::Duration::from_secs(10))
127+
.stream_events_from(active_trusted_relays().await, filter, std::time::Duration::from_secs(10))
128128
.await
129129
.map_err(|e| e.to_string())?;
130130

@@ -180,7 +180,7 @@ pub async fn get_invited_users(npub: String) -> Result<u32, String> {
180180
.limit(100);
181181

182182
let mut events = client
183-
.stream_events_from(TRUSTED_RELAYS.to_vec(), filter, std::time::Duration::from_secs(10))
183+
.stream_events_from(active_trusted_relays().await, filter, std::time::Duration::from_secs(10))
184184
.await
185185
.map_err(|e| e.to_string())?;
186186

@@ -205,7 +205,7 @@ pub async fn get_invited_users(npub: String) -> Result<u32, String> {
205205
.limit(1000); // Allow fetching many acceptances
206206

207207
let mut acceptance_events = client
208-
.stream_events_from(TRUSTED_RELAYS.to_vec(), acceptance_filter, std::time::Duration::from_secs(10))
208+
.stream_events_from(active_trusted_relays().await, acceptance_filter, std::time::Duration::from_secs(10))
209209
.await
210210
.map_err(|e| e.to_string())?;
211211

@@ -251,7 +251,7 @@ pub async fn check_fawkes_badge(npub: String) -> Result<bool, String> {
251251
.limit(10);
252252

253253
let mut events = client
254-
.stream_events_from(TRUSTED_RELAYS.to_vec(), filter, std::time::Duration::from_secs(10))
254+
.stream_events_from(active_trusted_relays().await, filter, std::time::Duration::from_secs(10))
255255
.await
256256
.map_err(|e| e.to_string())?;
257257

src-tauri/src/commands/mls.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use tauri::Emitter;
1313
use std::sync::Arc;
1414
#[cfg(not(target_os = "android"))]
1515
use tauri_plugin_fs::FsExt;
16-
use crate::{db, mls, MlsService, NotificationData, show_notification_generic, NOSTR_CLIENT, NOTIFIED_WELCOMES, STATE, TAURI_APP, TRUSTED_RELAYS};
16+
use crate::{db, mls, MlsService, NotificationData, show_notification_generic, NOSTR_CLIENT, NOTIFIED_WELCOMES, STATE, TAURI_APP, active_trusted_relays};
1717
use crate::util::{bytes_to_hex_string, hex_string_to_bytes};
1818

1919
// ============================================================================
@@ -91,7 +91,7 @@ pub async fn regenerate_device_keypackage(cache: bool) -> Result<serde_json::Val
9191
.limit(1);
9292

9393
match client.stream_events_from(
94-
TRUSTED_RELAYS.to_vec(),
94+
active_trusted_relays().await,
9595
filter,
9696
std::time::Duration::from_secs(5)
9797
).await {
@@ -123,14 +123,16 @@ pub async fn regenerate_device_keypackage(cache: bool) -> Result<serde_json::Val
123123
}
124124
}
125125

126+
// Resolve active relays before entering the no-await engine scope
127+
let relay_urls: Vec<nostr_sdk::RelayUrl> = active_trusted_relays().await
128+
.into_iter()
129+
.filter_map(|r| nostr_sdk::RelayUrl::parse(r).ok())
130+
.collect();
131+
126132
// Create device KeyPackage using persistent MLS engine inside a no-await scope
127133
let (kp_encoded, kp_tags) = {
128134
let mls_service = MlsService::new_persistent(&handle).map_err(|e| e.to_string())?;
129135
let engine = mls_service.engine().map_err(|e| e.to_string())?;
130-
let relay_urls: Vec<nostr_sdk::RelayUrl> = TRUSTED_RELAYS
131-
.iter()
132-
.filter_map(|r| nostr_sdk::RelayUrl::parse(r).ok())
133-
.collect();
134136
engine
135137
.create_key_package_for_event(&my_pubkey, relay_urls)
136138
.map_err(|e| e.to_string())?
@@ -161,7 +163,7 @@ pub async fn regenerate_device_keypackage(cache: bool) -> Result<serde_json::Val
161163
let mut send_result = None;
162164
let mut last_error = String::new();
163165
for attempt in 1..=3 {
164-
match client.send_event_to(TRUSTED_RELAYS.iter().copied(), &kp_event).await {
166+
match client.send_event_to(active_trusted_relays().await.into_iter(), &kp_event).await {
165167
Ok(result) => {
166168
// Check if at least one relay succeeded
167169
if !result.success.is_empty() {
@@ -428,7 +430,7 @@ pub async fn refresh_keypackages_for_contact(
428430

429431
// Fetch from TRUSTED_RELAYS with short timeout
430432
let mut events = client
431-
.stream_events_from(TRUSTED_RELAYS.to_vec(), filter, std::time::Duration::from_secs(10))
433+
.stream_events_from(active_trusted_relays().await, filter, std::time::Duration::from_secs(10))
432434
.await
433435
.map_err(|e| e.to_string())?;
434436

src-tauri/src/commands/realtime.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
use nostr_sdk::prelude::*;
88

9-
use crate::{mls, NOSTR_CLIENT, TRUSTED_RELAYS};
9+
use crate::{mls, NOSTR_CLIENT, active_trusted_relays};
1010

1111
// ============================================================================
1212
// Typing Indicators
@@ -47,7 +47,7 @@ pub async fn start_typing(receiver: String) -> bool {
4747
);
4848
match client
4949
.gift_wrap_to(
50-
TRUSTED_RELAYS.iter().copied(),
50+
active_trusted_relays().await.into_iter(),
5151
&pubkey,
5252
rumor,
5353
[Tag::expiration(expiry_time)],
@@ -133,7 +133,7 @@ pub async fn send_webxdc_peer_advertisement(
133133
);
134134
match client
135135
.gift_wrap_to(
136-
TRUSTED_RELAYS.iter().copied(),
136+
active_trusted_relays().await.into_iter(),
137137
&pubkey,
138138
rumor,
139139
[Tag::expiration(expiry_time)],

src-tauri/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ mod state;
7272
pub(crate) use state::{
7373
SyncMode,
7474
TAURI_APP, NOSTR_CLIENT, MY_KEYS, MY_PUBLIC_KEY, STATE,
75-
TRUSTED_RELAYS, NOTIFIED_WELCOMES, WRAPPER_ID_CACHE,
75+
TRUSTED_RELAYS, active_trusted_relays, NOTIFIED_WELCOMES, WRAPPER_ID_CACHE,
7676
MNEMONIC_SEED, ENCRYPTION_KEY, PENDING_NSEC, PENDING_INVITE,
7777
get_blossom_servers, PendingInviteAcceptance,
7878
};

0 commit comments

Comments
 (0)