feat: DM epoch fix, federation relay, and mDNS mesh discovery
- schema: createChannel returns wasNew :Bool to elect the MLS initiator unambiguously; prevents duplicate group creation on concurrent /dm calls - core: group helpers for epoch tracking and key-package lifecycle - server: federation subsystem — mTLS QUIC server-to-server relay with Cap'n Proto RPC; enqueue/batchEnqueue relay unknown recipients to their home domain via FederationClient - server: mDNS _quicproquo._udp.local. service announcement on startup - server: storage + sql_store — identity_exists, peek/ack, federation home-server lookup helpers - client: /mesh peers REPL command (mDNS discovery, feature = "mesh") - client: MeshDiscovery — background mDNS browse with ServiceDaemon - client: was_new=false path in cmd_dm waits for peer Welcome instead of creating a duplicate initiator group - p2p: fix ALPN from quicnprotochat/p2p/1 → quicproquo/p2p/1 - workspace: re-include quicproquo-p2p in members
This commit is contained in:
@@ -56,5 +56,8 @@ toml = { version = "0.8" }
|
||||
metrics = "0.22"
|
||||
metrics-exporter-prometheus = "0.15"
|
||||
|
||||
# mDNS service announcement for local mesh / Freifunk node discovery.
|
||||
mdns-sd = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -72,6 +72,7 @@ pub struct FederationPeerConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
pub struct EffectiveFederationConfig {
|
||||
pub enabled: bool,
|
||||
pub domain: String,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! A bare `username` (no `@`) is treated as local.
|
||||
|
||||
#![allow(dead_code)] // federation not yet wired up
|
||||
|
||||
/// A parsed federated address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FederatedAddress {
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
//!
|
||||
//! Uses a lazy connection pool (DashMap) to reuse QUIC connections to known peers.
|
||||
|
||||
#![allow(dead_code)] // federation not yet wired up
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use dashmap::DashMap;
|
||||
use quinn::Endpoint;
|
||||
|
||||
use crate::config::{EffectiveFederationConfig, FederationPeerConfig};
|
||||
use crate::config::EffectiveFederationConfig;
|
||||
|
||||
/// Outbound federation client for relaying to peer servers.
|
||||
pub struct FederationClient {
|
||||
|
||||
@@ -11,6 +11,4 @@ pub mod routing;
|
||||
pub mod service;
|
||||
pub mod tls;
|
||||
|
||||
pub use address::FederatedAddress;
|
||||
pub use client::FederationClient;
|
||||
pub use routing::Destination;
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn build_federation_server_config(
|
||||
let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13])
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_single_cert(cert_chain, key)?;
|
||||
tls.alpn_protocols = vec![b"qnpc-fed".to_vec()];
|
||||
tls.alpn_protocols = vec![b"quicproquo/federation/1".to_vec()];
|
||||
|
||||
let crypto = QuicServerConfig::try_from(tls)
|
||||
.map_err(|e| anyhow::anyhow!("invalid federation server TLS config: {e}"))?;
|
||||
|
||||
@@ -354,6 +354,65 @@ async fn main() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// ── mDNS local mesh discovery ─────────────────────────────────────────────
|
||||
// Announce this server on the local network so mesh-mode clients (and other
|
||||
// Freifunk nodes) can discover it automatically without manual configuration.
|
||||
// Non-critical: failures are logged as warnings; the server starts regardless.
|
||||
let _mdns_daemon = {
|
||||
let listen_port: u16 = listen.port();
|
||||
// Use the federation domain as the mDNS instance name when available.
|
||||
let mdns_instance = effective
|
||||
.federation
|
||||
.as_ref()
|
||||
.map(|f| f.domain.clone())
|
||||
.unwrap_or_else(|| "qpq-server".to_string());
|
||||
// mDNS host names must end with a dot.
|
||||
let mdns_host = if mdns_instance.ends_with('.') {
|
||||
mdns_instance.clone()
|
||||
} else {
|
||||
format!("{mdns_instance}.local.")
|
||||
};
|
||||
|
||||
match mdns_sd::ServiceDaemon::new() {
|
||||
Ok(daemon) => {
|
||||
let mut props = std::collections::HashMap::new();
|
||||
props.insert("ver".to_string(), "1".to_string());
|
||||
props.insert("server".to_string(), effective.listen.clone());
|
||||
props.insert("domain".to_string(), mdns_instance.clone());
|
||||
|
||||
match mdns_sd::ServiceInfo::new(
|
||||
"_quicproquo._udp.local.",
|
||||
&mdns_instance,
|
||||
&mdns_host,
|
||||
&[] as &[std::net::IpAddr],
|
||||
listen_port,
|
||||
Some(props),
|
||||
) {
|
||||
Ok(info) => match daemon.register(info) {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
instance = %mdns_instance,
|
||||
port = listen_port,
|
||||
"mDNS: announced qpq server on local network (_quicproquo._udp.local.)"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "mDNS: service registration failed; mesh discovery disabled");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "mDNS: failed to build service info; mesh discovery disabled");
|
||||
}
|
||||
}
|
||||
Some(daemon)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "mDNS: daemon start failed; mesh discovery disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// capnp-rpc is !Send (Rc internals), so all RPC tasks must stay on a LocalSet.
|
||||
let local = LocalSet::new();
|
||||
local
|
||||
|
||||
@@ -51,12 +51,14 @@ impl NodeServiceImpl {
|
||||
));
|
||||
}
|
||||
|
||||
let channel_id = match self.store.create_channel(&identity, &peer_key) {
|
||||
Ok(id) => id,
|
||||
let (channel_id, was_new) = match self.store.create_channel(&identity, &peer_key) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return Promise::err(storage_err(e)),
|
||||
};
|
||||
|
||||
results.get().set_channel_id(&channel_id);
|
||||
let mut r = results.get();
|
||||
r.set_channel_id(&channel_id);
|
||||
r.set_was_new(was_new);
|
||||
Promise::ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,42 @@ impl NodeServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Federation routing: if the recipient's home server differs from ours, relay the
|
||||
// message to the remote server instead of enqueueing locally. This enables
|
||||
// cross-node delivery in a Freifunk / community mesh deployment.
|
||||
if let (Some(fed_client), Some(local_domain)) =
|
||||
(&self.federation_client, &self.local_domain)
|
||||
{
|
||||
let dest = crate::federation::routing::resolve_destination(
|
||||
&self.store,
|
||||
&recipient_key,
|
||||
local_domain,
|
||||
);
|
||||
if let crate::federation::routing::Destination::Remote(remote_domain) = dest {
|
||||
let fed = Arc::clone(fed_client);
|
||||
let rk = recipient_key;
|
||||
let pl = payload;
|
||||
let ch = channel_id;
|
||||
tracing::info!(
|
||||
recipient_prefix = %fmt_hex(&rk[..4]),
|
||||
domain = %remote_domain,
|
||||
"federation: routing enqueue to remote server"
|
||||
);
|
||||
return Promise::from_future(async move {
|
||||
let seq = fed
|
||||
.relay_enqueue(&remote_domain, &rk, &pl, &ch)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
capnp::Error::failed(format!("federation relay failed: {e}"))
|
||||
})?;
|
||||
results.get().set_seq(seq);
|
||||
metrics::record_enqueue_total();
|
||||
metrics::record_enqueue_bytes(pl.len() as u64);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// DM channel authz: channel_id.len() == 16 means a created channel; caller and recipient must be the two members.
|
||||
if channel_id.len() == 16 {
|
||||
let members = match self.store.get_channel_members(&channel_id) {
|
||||
@@ -591,7 +627,8 @@ impl NodeServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
let mut seqs = Vec::with_capacity(recipient_keys.len() as usize);
|
||||
// Eagerly collect recipient keys so params can be dropped before any async work.
|
||||
let mut recipient_key_vecs: Vec<Vec<u8>> = Vec::with_capacity(recipient_keys.len() as usize);
|
||||
for i in 0..recipient_keys.len() {
|
||||
let rk = match recipient_keys.get(i) {
|
||||
Ok(v) => v.to_vec(),
|
||||
@@ -604,7 +641,7 @@ impl NodeServiceImpl {
|
||||
));
|
||||
}
|
||||
|
||||
// Per-recipient DM channel membership check.
|
||||
// Per-recipient DM channel membership check (only when channel_id is a 16-byte UUID).
|
||||
if channel_id.len() == 16 {
|
||||
let members = match self.store.get_channel_members(&channel_id) {
|
||||
Ok(Some(m)) => m,
|
||||
@@ -631,44 +668,79 @@ impl NodeServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
match self.store.queue_depth(&rk, &channel_id) {
|
||||
Ok(depth) if depth >= MAX_QUEUE_DEPTH => {
|
||||
return Promise::err(coded_error(
|
||||
E015_QUEUE_FULL,
|
||||
format!("queue depth {} exceeds limit {}", depth, MAX_QUEUE_DEPTH),
|
||||
));
|
||||
}
|
||||
Err(e) => return Promise::err(storage_err(e)),
|
||||
_ => {}
|
||||
recipient_key_vecs.push(rk);
|
||||
}
|
||||
|
||||
let n = recipient_key_vecs.len();
|
||||
let store = Arc::clone(&self.store);
|
||||
let waiters = Arc::clone(&self.waiters);
|
||||
let fed_client = self.federation_client.clone();
|
||||
let local_domain = self.local_domain.clone();
|
||||
|
||||
// Use an async future to support federation relay alongside local enqueue.
|
||||
// All storage operations are synchronous; only federation relay calls are await-ed.
|
||||
Promise::from_future(async move {
|
||||
let mut seqs = Vec::with_capacity(n);
|
||||
|
||||
for rk in &recipient_key_vecs {
|
||||
// Federation routing: relay to the recipient's home server when remote.
|
||||
let dest = if let (Some(ref _fed), Some(ref domain)) = (&fed_client, &local_domain) {
|
||||
crate::federation::routing::resolve_destination(&store, rk, domain)
|
||||
} else {
|
||||
crate::federation::routing::Destination::Local
|
||||
};
|
||||
|
||||
let seq = match dest {
|
||||
crate::federation::routing::Destination::Remote(ref remote_domain) => {
|
||||
let fed = fed_client.as_deref().ok_or_else(|| {
|
||||
capnp::Error::failed("federation client unavailable for remote routing".into())
|
||||
})?;
|
||||
tracing::info!(
|
||||
recipient_prefix = %fmt_hex(&rk[..4]),
|
||||
domain = %remote_domain,
|
||||
"federation: routing batch enqueue to remote server"
|
||||
);
|
||||
fed.relay_enqueue(remote_domain, rk, &payload, &channel_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
capnp::Error::failed(format!("federation relay failed: {e}"))
|
||||
})?
|
||||
}
|
||||
crate::federation::routing::Destination::Local => {
|
||||
match store.queue_depth(rk, &channel_id) {
|
||||
Ok(depth) if depth >= MAX_QUEUE_DEPTH => {
|
||||
return Err(coded_error(
|
||||
E015_QUEUE_FULL,
|
||||
format!("queue depth {} exceeds limit {MAX_QUEUE_DEPTH}", depth),
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(storage_err(e)),
|
||||
_ => {}
|
||||
}
|
||||
store
|
||||
.enqueue(rk, &channel_id, payload.clone())
|
||||
.map_err(storage_err)?
|
||||
}
|
||||
};
|
||||
|
||||
seqs.push(seq);
|
||||
metrics::record_enqueue_total();
|
||||
metrics::record_enqueue_bytes(payload.len() as u64);
|
||||
crate::auth::waiter(&waiters, rk).notify_waiters();
|
||||
}
|
||||
|
||||
let seq = match self
|
||||
.store
|
||||
.enqueue(&rk, &channel_id, payload.clone())
|
||||
.map_err(storage_err)
|
||||
{
|
||||
Ok(seq) => seq,
|
||||
Err(e) => return Promise::err(e),
|
||||
};
|
||||
seqs.push(seq);
|
||||
let mut list = results.get().init_seqs(seqs.len() as u32);
|
||||
for (i, seq) in seqs.iter().enumerate() {
|
||||
list.set(i as u32, *seq);
|
||||
}
|
||||
|
||||
metrics::record_enqueue_total();
|
||||
metrics::record_enqueue_bytes(payload.len() as u64);
|
||||
tracing::info!(
|
||||
recipient_count = n,
|
||||
payload_len = payload.len(),
|
||||
"audit: batch_enqueue"
|
||||
);
|
||||
|
||||
crate::auth::waiter(&self.waiters, &rk).notify_waiters();
|
||||
}
|
||||
|
||||
let mut list = results.get().init_seqs(seqs.len() as u32);
|
||||
for (i, seq) in seqs.iter().enumerate() {
|
||||
list.set(i as u32, *seq);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
recipient_count = recipient_keys.len(),
|
||||
payload_len = payload.len(),
|
||||
"audit: batch_enqueue"
|
||||
);
|
||||
|
||||
Promise::ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ impl Store for SqlStore {
|
||||
.map_err(|e| StorageError::Db(e.to_string()))
|
||||
}
|
||||
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<Vec<u8>, StorageError> {
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<(Vec<u8>, bool), StorageError> {
|
||||
let (a, b) = if member_a < member_b {
|
||||
(member_a.to_vec(), member_b.to_vec())
|
||||
} else {
|
||||
@@ -473,7 +473,7 @@ impl Store for SqlStore {
|
||||
.optional()
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
if let Some(id) = existing {
|
||||
return Ok(id);
|
||||
return Ok((id, false));
|
||||
}
|
||||
let mut channel_id = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut channel_id);
|
||||
@@ -482,7 +482,7 @@ impl Store for SqlStore {
|
||||
params![channel_id.as_slice(), a, b],
|
||||
)
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
Ok(channel_id.to_vec())
|
||||
Ok((channel_id.to_vec(), true))
|
||||
}
|
||||
|
||||
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError> {
|
||||
@@ -721,4 +721,107 @@ mod tests {
|
||||
let b_msgs = store.fetch(&rk, b"ch-b").unwrap();
|
||||
assert_eq!(b_msgs, vec![(0u64, b"b1".to_vec())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_was_new_first_call() {
|
||||
let store = open_in_memory();
|
||||
let a = [10u8; 32];
|
||||
let b = [11u8; 32];
|
||||
let (id, was_new) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id.len(), 16, "channel_id must be 16 bytes");
|
||||
assert!(was_new, "first create_channel must return was_new=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_idempotent_same_direction() {
|
||||
let store = open_in_memory();
|
||||
let a = [12u8; 32];
|
||||
let b = [13u8; 32];
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id1, id2, "repeated call must return same channel_id");
|
||||
assert!(was_new1);
|
||||
assert!(!was_new2, "second call must return was_new=false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_idempotent_reversed_direction() {
|
||||
let store = open_in_memory();
|
||||
let a = [14u8; 32];
|
||||
let b = [15u8; 32];
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&b, &a).unwrap();
|
||||
assert_eq!(id1, id2, "reversed-key call must return same channel_id");
|
||||
assert!(was_new1);
|
||||
assert!(!was_new2, "reversed-key second call must return was_new=false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_different_pairs_isolated() {
|
||||
let store = open_in_memory();
|
||||
let a = [16u8; 32];
|
||||
let b = [17u8; 32];
|
||||
let c = [18u8; 32];
|
||||
let (id_ab, _) = store.create_channel(&a, &b).unwrap();
|
||||
let (id_ac, _) = store.create_channel(&a, &c).unwrap();
|
||||
let (id_bc, _) = store.create_channel(&b, &c).unwrap();
|
||||
assert_ne!(id_ab, id_ac);
|
||||
assert_ne!(id_ab, id_bc);
|
||||
assert_ne!(id_ac, id_bc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_get_members_roundtrip() {
|
||||
let store = open_in_memory();
|
||||
let a = [20u8; 32];
|
||||
let b = [21u8; 32];
|
||||
let (id, _) = store.create_channel(&a, &b).unwrap();
|
||||
let members = store.get_channel_members(&id).unwrap();
|
||||
assert!(members.is_some(), "get_channel_members must return Some after create");
|
||||
let (ma, mb) = members.unwrap();
|
||||
// members stored in canonical (lex) order
|
||||
let (expected_a, expected_b) = if a < b {
|
||||
(a.to_vec(), b.to_vec())
|
||||
} else {
|
||||
(b.to_vec(), a.to_vec())
|
||||
};
|
||||
assert_eq!(ma, expected_a);
|
||||
assert_eq!(mb, expected_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_channel_members_unknown_id_returns_none() {
|
||||
let store = open_in_memory();
|
||||
assert!(store.get_channel_members(&[0u8; 16]).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_identity_key_after_store() {
|
||||
let store = open_in_memory();
|
||||
let ik = [30u8; 32];
|
||||
store.store_user_record("carol", b"record".to_vec()).unwrap();
|
||||
store.store_user_identity_key("carol", ik.to_vec()).unwrap();
|
||||
let resolved = store.resolve_identity_key(&ik).unwrap();
|
||||
assert_eq!(resolved, Some("carol".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_identity_key_unknown_returns_none() {
|
||||
let store = open_in_memory();
|
||||
let unknown = [31u8; 32];
|
||||
assert!(store.resolve_identity_key(&unknown).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_identity_key_two_users_distinct() {
|
||||
let store = open_in_memory();
|
||||
let ik_a = [32u8; 32];
|
||||
let ik_b = [33u8; 32];
|
||||
store.store_user_record("user_a", b"ra".to_vec()).unwrap();
|
||||
store.store_user_record("user_b", b"rb".to_vec()).unwrap();
|
||||
store.store_user_identity_key("user_a", ik_a.to_vec()).unwrap();
|
||||
store.store_user_identity_key("user_b", ik_b.to_vec()).unwrap();
|
||||
assert_eq!(store.resolve_identity_key(&ik_a).unwrap(), Some("user_a".to_string()));
|
||||
assert_eq!(store.resolve_identity_key(&ik_b).unwrap(), Some("user_b".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +127,12 @@ pub trait Store: Send + Sync {
|
||||
/// Resolve a peer's P2P endpoint address.
|
||||
fn resolve_endpoint(&self, identity_key: &[u8]) -> Result<Option<Vec<u8>>, StorageError>;
|
||||
|
||||
/// Create a 1:1 channel between two members. Returns 16-byte channel_id (UUID).
|
||||
/// Members are stored in sorted order for deterministic lookup.
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<Vec<u8>, StorageError>;
|
||||
/// Create a 1:1 channel between two members.
|
||||
/// Returns `(channel_id, was_new)` where `was_new` is true iff the channel was created by
|
||||
/// this call (false = it already existed). Members are stored in sorted order for deterministic
|
||||
/// lookup — both `create_channel(a, b)` and `create_channel(b, a)` return the same channel_id.
|
||||
/// The caller who receives `was_new = true` is the MLS group initiator and must send the Welcome.
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<(Vec<u8>, bool), StorageError>;
|
||||
|
||||
/// Get the two members of a channel by channel_id (16 bytes). Returns (member_a, member_b) in sorted order.
|
||||
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError>;
|
||||
@@ -137,6 +140,7 @@ pub trait Store: Send + Sync {
|
||||
// ── Federation ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Store the home server domain for an identity key.
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
fn store_identity_home_server(
|
||||
&self,
|
||||
identity_key: &[u8],
|
||||
@@ -157,6 +161,7 @@ pub trait Store: Send + Sync {
|
||||
) -> Result<(), StorageError>;
|
||||
|
||||
/// List all active federation peers.
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
fn list_federation_peers(&self) -> Result<Vec<(String, bool)>, StorageError>;
|
||||
}
|
||||
|
||||
@@ -647,7 +652,7 @@ impl Store for FileBackedStore {
|
||||
Ok(map.get(identity_key).cloned())
|
||||
}
|
||||
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<Vec<u8>, StorageError> {
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<(Vec<u8>, bool), StorageError> {
|
||||
let (a, b) = if member_a < member_b {
|
||||
(member_a.to_vec(), member_b.to_vec())
|
||||
} else {
|
||||
@@ -655,14 +660,14 @@ impl Store for FileBackedStore {
|
||||
};
|
||||
let mut map = lock(&self.channels)?;
|
||||
if let Some((channel_id, _)) = map.iter().find(|(_, (ma, mb))| ma == &a && mb == &b) {
|
||||
return Ok(channel_id.clone());
|
||||
return Ok((channel_id.clone(), false));
|
||||
}
|
||||
let mut channel_id = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut channel_id);
|
||||
let channel_id = channel_id.to_vec();
|
||||
map.insert(channel_id.clone(), (a, b));
|
||||
self.flush_channels(&self.channels_path, &*map)?;
|
||||
Ok(channel_id)
|
||||
Ok((channel_id, true))
|
||||
}
|
||||
|
||||
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError> {
|
||||
@@ -812,12 +817,40 @@ mod tests {
|
||||
let a = vec![1u8; 32];
|
||||
let b = vec![2u8; 32];
|
||||
assert_eq!(store.get_channel_members(&[0u8; 16]).unwrap(), None);
|
||||
let id1 = store.create_channel(&a, &b).unwrap();
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id1.len(), 16);
|
||||
assert!(was_new1, "first call must return was_new=true");
|
||||
let members = store.get_channel_members(&id1).unwrap().unwrap();
|
||||
assert_eq!(members.0, a);
|
||||
assert_eq!(members.1, b);
|
||||
let id2 = store.create_channel(&b, &a).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&b, &a).unwrap();
|
||||
assert_eq!(id1, id2, "reversed key order must return same channel_id");
|
||||
assert!(!was_new2, "second call (reversed) must return was_new=false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_idempotent_same_direction() {
|
||||
let (_dir, store) = temp_store();
|
||||
let a = vec![3u8; 32];
|
||||
let b = vec![4u8; 32];
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id1, id2);
|
||||
assert!(was_new1);
|
||||
assert!(!was_new2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_different_pairs_get_different_ids() {
|
||||
let (_dir, store) = temp_store();
|
||||
let a = vec![5u8; 32];
|
||||
let b = vec![6u8; 32];
|
||||
let c = vec![7u8; 32];
|
||||
let (id_ab, _) = store.create_channel(&a, &b).unwrap();
|
||||
let (id_ac, _) = store.create_channel(&a, &c).unwrap();
|
||||
let (id_bc, _) = store.create_channel(&b, &c).unwrap();
|
||||
assert_ne!(id_ab, id_ac);
|
||||
assert_ne!(id_ab, id_bc);
|
||||
assert_ne!(id_ac, id_bc);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user