Skip to content

Commit 84b1a01

Browse files
erskingardneryukibtc
authored andcommitted
mls: replace save_group_relay with replace_group_relays
This PR fixes a bug where Group relays weren't being updated by NostrDataExtension updates. Additionally, I've created a simple test framework for testing both nostr-mls storage adapters against the same tests to ensure correctness and consistency. Pull-Request: #1056 Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent c4a38cd commit 84b1a01

File tree

21 files changed

+1782
-133
lines changed

21 files changed

+1782
-133
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mls/nostr-mls-memory-storage/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
### Breaking changes
2929

3030
- Remove group type from groups
31+
- Replaced `save_group_relay` with `replace_group_relays` trait method (https://github.com/rust-nostr/nostr/pull/1056)
3132

3233
### Changed
3334

mls/nostr-mls-memory-storage/src/groups.rs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::collections::BTreeSet;
44

5-
use nostr::PublicKey;
5+
use nostr::{PublicKey, RelayUrl};
66
use nostr_mls_storage::groups::error::{GroupError, InvalidGroupState};
77
use nostr_mls_storage::groups::types::*;
88
use nostr_mls_storage::groups::GroupStorage;
@@ -72,37 +72,47 @@ impl GroupStorage for NostrMlsMemoryStorage {
7272

7373
fn group_relays(&self, mls_group_id: &GroupId) -> Result<BTreeSet<GroupRelay>, GroupError> {
7474
// Check if the group exists first
75-
self.find_group_by_mls_group_id(mls_group_id)?;
75+
if self.find_group_by_mls_group_id(mls_group_id)?.is_none() {
76+
return Err(GroupError::InvalidParameters(format!(
77+
"Group with MLS ID {:?} not found",
78+
mls_group_id
79+
)));
80+
}
7681

7782
let cache = self.group_relays_cache.read();
7883
match cache.peek(mls_group_id).cloned() {
7984
Some(relays) => Ok(relays),
80-
None => Err(GroupError::InvalidState(InvalidGroupState::NoRelays)),
85+
// If not in cache but group exists, return empty set
86+
None => Ok(BTreeSet::new()),
8187
}
8288
}
8389

84-
fn save_group_relay(&self, group_relay: GroupRelay) -> Result<(), GroupError> {
90+
fn replace_group_relays(
91+
&self,
92+
group_id: &GroupId,
93+
relays: BTreeSet<RelayUrl>,
94+
) -> Result<(), GroupError> {
8595
// Check if the group exists first
86-
self.find_group_by_mls_group_id(&group_relay.mls_group_id)?;
96+
if self.find_group_by_mls_group_id(group_id)?.is_none() {
97+
return Err(GroupError::InvalidParameters(format!(
98+
"Group with MLS ID {:?} not found",
99+
group_id
100+
)));
101+
}
87102

88103
let mut cache = self.group_relays_cache.write();
89104

90-
// Try to get the existing relays for the group
91-
match cache.get_mut(&group_relay.mls_group_id) {
92-
// If the group exists, add the new relay to the vector
93-
Some(existing_relays) => {
94-
// Add the new relay if it doesn't already exist
95-
existing_relays.insert(group_relay);
96-
}
97-
// If the group doesn't exist, create a new vector with the new relay
98-
None => {
99-
// Update the cache with the new vector
100-
cache.put(
101-
group_relay.mls_group_id.clone(),
102-
BTreeSet::from([group_relay]),
103-
);
104-
}
105-
};
105+
// Convert RelayUrl set to GroupRelay set
106+
let group_relays: BTreeSet<GroupRelay> = relays
107+
.into_iter()
108+
.map(|relay_url| GroupRelay {
109+
mls_group_id: group_id.clone(),
110+
relay_url,
111+
})
112+
.collect();
113+
114+
// Replace the entire relay set for this group
115+
cache.put(group_id.clone(), group_relays);
106116

107117
Ok(())
108118
}

mls/nostr-mls-memory-storage/src/lib.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -310,19 +310,9 @@ mod tests {
310310
nostr_storage.save_group(group.clone()).unwrap();
311311
let relay_url1 = RelayUrl::parse("wss://relay1.example.com").unwrap();
312312
let relay_url2 = RelayUrl::parse("wss://relay2.example.com").unwrap();
313-
let group_relay1 = GroupRelay {
314-
mls_group_id: mls_group_id.clone(),
315-
relay_url: relay_url1,
316-
};
317-
let group_relay2 = GroupRelay {
318-
mls_group_id: mls_group_id.clone(),
319-
relay_url: relay_url2,
320-
};
313+
let relays = BTreeSet::from([relay_url1, relay_url2]);
321314
nostr_storage
322-
.save_group_relay(group_relay1.clone())
323-
.unwrap();
324-
nostr_storage
325-
.save_group_relay(group_relay2.clone())
315+
.replace_group_relays(&mls_group_id, relays)
326316
.unwrap();
327317
let found_relays = nostr_storage.group_relays(&mls_group_id).unwrap();
328318
assert_eq!(found_relays.len(), 2);
@@ -337,11 +327,6 @@ mod tests {
337327
panic!("Group relays not found in cache");
338328
}
339329
}
340-
nostr_storage
341-
.save_group_relay(group_relay1.clone())
342-
.unwrap();
343-
let found_relays_after_duplicate = nostr_storage.group_relays(&mls_group_id).unwrap();
344-
assert_eq!(found_relays_after_duplicate.len(), 2);
345330
}
346331

347332
#[test]

mls/nostr-mls-sqlite-storage/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
### Breaking changes
2929

3030
- Remove group type from groups
31+
- Replaced `save_group_relay` with `replace_group_relays` trait method (https://github.com/rust-nostr/nostr/pull/1056)
3132

3233
### Changed
3334

mls/nostr-mls-sqlite-storage/src/groups.rs

Lines changed: 29 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::collections::BTreeSet;
44

5-
use nostr::PublicKey;
5+
use nostr::{PublicKey, RelayUrl};
66
use nostr_mls_storage::groups::error::GroupError;
77
use nostr_mls_storage::groups::types::{Group, GroupExporterSecret, GroupRelay};
88
use nostr_mls_storage::groups::GroupStorage;
@@ -192,29 +192,42 @@ impl GroupStorage for NostrMlsSqliteStorage {
192192
Ok(relays)
193193
}
194194

195-
fn save_group_relay(&self, group_relay: GroupRelay) -> Result<(), GroupError> {
195+
fn replace_group_relays(
196+
&self,
197+
group_id: &GroupId,
198+
relays: BTreeSet<RelayUrl>,
199+
) -> Result<(), GroupError> {
196200
// First verify the group exists
197-
if self
198-
.find_group_by_mls_group_id(&group_relay.mls_group_id)?
199-
.is_none()
200-
{
201+
if self.find_group_by_mls_group_id(group_id)?.is_none() {
201202
return Err(GroupError::InvalidParameters(format!(
202203
"Group with MLS ID {:?} not found",
203-
group_relay.mls_group_id
204+
group_id
204205
)));
205206
}
206207

207208
let conn_guard = self.db_connection.lock().map_err(into_group_err)?;
208209

209-
conn_guard
210-
.execute(
211-
"INSERT OR REPLACE INTO group_relays (mls_group_id, relay_url) VALUES (?, ?)",
212-
params![
213-
group_relay.mls_group_id.as_slice(),
214-
group_relay.relay_url.as_str()
215-
],
210+
// Use a transaction for atomicity
211+
let tx = conn_guard.unchecked_transaction().map_err(into_group_err)?;
212+
213+
// Clear existing relays for this group
214+
tx.execute(
215+
"DELETE FROM group_relays WHERE mls_group_id = ?",
216+
params![group_id.as_slice()],
217+
)
218+
.map_err(into_group_err)?;
219+
220+
// Insert new relays
221+
for relay_url in relays {
222+
tx.execute(
223+
"INSERT INTO group_relays (mls_group_id, relay_url) VALUES (?, ?)",
224+
params![group_id.as_slice(), relay_url.as_str()],
216225
)
217226
.map_err(into_group_err)?;
227+
}
228+
229+
// Commit the transaction
230+
tx.commit().map_err(into_group_err)?;
218231

219232
Ok(())
220233
}
@@ -276,7 +289,6 @@ impl GroupStorage for NostrMlsSqliteStorage {
276289
mod tests {
277290
use aes_gcm::aead::OsRng;
278291
use aes_gcm::{Aes128Gcm, KeyInit};
279-
use nostr::RelayUrl;
280292
use nostr_mls_storage::groups::types::GroupState;
281293

282294
use super::*;
@@ -335,51 +347,8 @@ mod tests {
335347
assert_eq!(all_groups.len(), 1);
336348
}
337349

338-
#[test]
339-
fn test_group_relay() {
340-
let storage = NostrMlsSqliteStorage::new_in_memory().unwrap();
341-
342-
// Create a test group
343-
let mls_group_id = GroupId::from_slice(&[1, 2, 3, 4]);
344-
let mut nostr_group_id = [0u8; 32];
345-
nostr_group_id[0..13].copy_from_slice(b"test_group_12");
346-
347-
let group = Group {
348-
mls_group_id: mls_group_id.clone(),
349-
nostr_group_id,
350-
name: "Test Group".to_string(),
351-
description: "A test group".to_string(),
352-
admin_pubkeys: BTreeSet::new(),
353-
last_message_id: None,
354-
last_message_at: None,
355-
epoch: 0,
356-
state: GroupState::Active,
357-
image_url: None,
358-
image_key: None,
359-
image_nonce: None,
360-
};
361-
362-
// Save the group
363-
storage.save_group(group).unwrap();
364-
365-
// Create a group relay
366-
let relay_url = RelayUrl::parse("wss://relay.example.com").unwrap();
367-
let group_relay = GroupRelay {
368-
mls_group_id: mls_group_id.clone(),
369-
relay_url,
370-
};
371-
372-
// Save the group relay
373-
storage.save_group_relay(group_relay).unwrap();
374-
375-
// Get group relays
376-
let relays = storage.group_relays(&mls_group_id).unwrap();
377-
assert_eq!(relays.len(), 1);
378-
assert_eq!(
379-
relays.first().unwrap().relay_url.to_string(),
380-
"wss://relay.example.com"
381-
);
382-
}
350+
// Note: Comprehensive storage functionality tests are now in nostr-mls-storage/tests/
351+
// using shared test functions to ensure consistency between storage implementations
383352

384353
#[test]
385354
fn test_group_exporter_secret() {

mls/nostr-mls-storage/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@
2828
### Breaking changes
2929

3030
- Remove group type from groups
31+
- Remove `save_group_relay` method (https://github.com/rust-nostr/nostr/pull/1056)
3132

3233
### Changed
3334

3435
- Upgrade openmls to v0.7.0
3536

37+
### Added
38+
39+
- Added `replace_group_relays` to make relay replace for groups an atomic operation (https://github.com/rust-nostr/nostr/pull/1056)
40+
- Comprehensive consistency testing framework for testing all nostr-mls-storage implementations for correctness and consistency (https://github.com/rust-nostr/nostr/pull/1056)
41+
3642
## v0.43.0 - 2025/07/28
3743

3844
No notable changes in this release.

mls/nostr-mls-storage/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ openmls = { version = "0.7.0", default-features = false }
1717
openmls_traits = { version = "0.4.0", default-features = false }
1818
serde = { workspace = true, features = ["derive"] }
1919
serde_json = { workspace = true, features = ["std"] }
20+
21+
[dev-dependencies]
22+
nostr-mls-sqlite-storage = { version = "0.43", path = "../nostr-mls-sqlite-storage" }
23+
nostr-mls-memory-storage = { version = "0.43", path = "../nostr-mls-memory-storage" }
24+
openmls_memory_storage = "0.4"

mls/nostr-mls-storage/src/groups/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
use std::collections::BTreeSet;
1111

12-
use nostr::PublicKey;
12+
use nostr::{PublicKey, RelayUrl};
1313
use openmls::group::GroupId;
1414

1515
pub mod error;
@@ -45,8 +45,13 @@ pub trait GroupStorage {
4545
/// Get all relays for a group
4646
fn group_relays(&self, group_id: &GroupId) -> Result<BTreeSet<GroupRelay>, GroupError>;
4747

48-
/// Save a group relay
49-
fn save_group_relay(&self, group_relay: GroupRelay) -> Result<(), GroupError>;
48+
/// Replace all relays for a group with the provided set
49+
/// This operation is atomic - either all relays are replaced or none are changed
50+
fn replace_group_relays(
51+
&self,
52+
group_id: &GroupId,
53+
relays: BTreeSet<RelayUrl>,
54+
) -> Result<(), GroupError>;
5055

5156
/// Get an exporter secret for a group and epoch
5257
fn get_group_exporter_secret(

mls/nostr-mls-storage/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub mod groups;
1111
pub mod messages;
1212
pub mod welcomes;
1313

14+
pub mod test_utils;
15+
1416
use self::groups::GroupStorage;
1517
use self::messages::MessageStorage;
1618
use self::welcomes::WelcomeStorage;

0 commit comments

Comments
 (0)