feat: add delivery sequence numbers + major server/client refactor

Delivery sequence numbers (MLS epoch ordering fix):
- schemas/node.capnp: add Envelope{seq,data} struct; enqueue returns seq:UInt64;
  fetch/fetchWait return List(Envelope) instead of List(Data)
- storage.rs: Store trait enqueue returns u64; fetch/fetch_limited return
  Vec<(u64, Vec<u8>)>; FileBackedStore gains QueueMapV3 with per-inbox seq
  counters and V2→V3 on-disk migration
- migrations/002_add_seq.sql: seq column, delivery_seq_counters table, index
- sql_store.rs: atomic UPSERT counter via RETURNING, ORDER BY seq, SCHEMA_VERSION→3
- node_service/delivery.rs: builds Envelope list; returns seq from enqueue
- client/rpc.rs: enqueue→u64, fetch_all/fetch_wait→Vec<(u64,Vec<u8>)>
- client/commands.rs: sort-by-seq before MLS processing; retry loop in cmd_recv
  and receive_pending_plaintexts for correct epoch ordering

Server refactor:
- Split monolithic main.rs into node_service/{mod,delivery,auth_ops,key_ops,p2p_ops}
- Add auth.rs (token validation, rate limiting), config.rs, metrics.rs, tls.rs
- Add SQL migrations runner (001_initial.sql, 002_add_seq.sql)
- OPAQUE PAKE login/registration, sealed-sender mode, queue depth limit (1000)

Client refactor:
- Split lib.rs into client/{commands,rpc,state,retry,hex,mod}
- Add cmd_whoami, cmd_health, cmd_check_key, cmd_ping subcommands
- Add cmd_register_user, cmd_login (OPAQUE), cmd_refresh_keypackage
- Hybrid PQ envelope (X25519 + ML-KEM-768) on all send/recv paths
- E2E test suite expanded

Other:
- quicnprotochat-gui: Tauri 2 desktop GUI skeleton (backend + HTML UI)
- quicnprotochat-p2p: iroh-based P2P transport stub
- quicnprotochat-core: app_message, hybrid_crypto modules; GroupMember API updates
- .github/workflows/size-lint.yml: binary size regression check
- docs: protocol comparison, roadmap updates, fully-operational checklist
This commit is contained in:
2026-02-22 20:40:12 +01:00
parent b5b361e2ff
commit 6b8b61c6ae
56 changed files with 10693 additions and 3024 deletions

View File

@@ -32,22 +32,30 @@ pub trait Store: Send + Sync {
fn fetch_key_package(&self, identity_key: &[u8]) -> Result<Option<Vec<u8>>, StorageError>;
/// Enqueue a payload and return the monotonically increasing per-inbox sequence number
/// assigned to this message. Clients sort by seq before MLS processing.
fn enqueue(
&self,
recipient_key: &[u8],
channel_id: &[u8],
payload: Vec<u8>,
) -> Result<(), StorageError>;
) -> Result<u64, StorageError>;
fn fetch(&self, recipient_key: &[u8], channel_id: &[u8]) -> Result<Vec<Vec<u8>>, StorageError>;
/// Fetch and drain all queued messages, returning `(seq, payload)` pairs ordered by seq.
fn fetch(
&self,
recipient_key: &[u8],
channel_id: &[u8],
) -> Result<Vec<(u64, Vec<u8>)>, StorageError>;
/// Fetch up to `limit` messages without draining the entire queue (Fix 8).
/// Returns `(seq, payload)` pairs ordered by seq.
fn fetch_limited(
&self,
recipient_key: &[u8],
channel_id: &[u8],
limit: usize,
) -> Result<Vec<Vec<u8>>, StorageError>;
) -> Result<Vec<(u64, Vec<u8>)>, StorageError>;
/// Return the number of queued messages for (recipient, channel) (Fix 7).
fn queue_depth(&self, recipient_key: &[u8], channel_id: &[u8]) -> Result<usize, StorageError>;
@@ -123,6 +131,19 @@ struct QueueMapV2 {
map: HashMap<ChannelKey, VecDeque<Vec<u8>>>,
}
#[derive(Serialize, Deserialize, Default, Clone)]
struct SeqEntry {
seq: u64,
data: Vec<u8>,
}
/// V3 delivery store: each queue entry carries a monotonic per-inbox sequence number.
#[derive(Serialize, Deserialize, Default)]
struct QueueMapV3 {
map: HashMap<ChannelKey, VecDeque<SeqEntry>>,
next_seq: HashMap<ChannelKey, u64>,
}
/// File-backed storage for KeyPackages and delivery queues.
///
/// Each mutation flushes the entire map to disk. Suitable for MVP-scale loads.
@@ -134,7 +155,7 @@ pub struct FileBackedStore {
users_path: PathBuf,
identity_keys_path: PathBuf,
key_packages: Mutex<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>,
deliveries: Mutex<HashMap<ChannelKey, VecDeque<Vec<u8>>>>,
deliveries: Mutex<QueueMapV3>,
hybrid_keys: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
users: Mutex<HashMap<String, Vec<u8>>>,
identity_keys: Mutex<HashMap<String, Vec<u8>>>,
@@ -155,7 +176,7 @@ impl FileBackedStore {
let identity_keys_path = dir.join("identity_keys.bin");
let key_packages = Mutex::new(Self::load_kp_map(&kp_path)?);
let deliveries = Mutex::new(Self::load_delivery_map(&ds_path)?);
let deliveries = Mutex::new(Self::load_delivery_map_v3(&ds_path)?);
let hybrid_keys = Mutex::new(Self::load_hybrid_keys(&hk_path)?);
let users = Mutex::new(Self::load_users(&users_path)?);
let identity_keys = Mutex::new(Self::load_map_string_bytes(&identity_keys_path)?);
@@ -201,28 +222,38 @@ impl FileBackedStore {
fs::write(path, bytes).map_err(|e| StorageError::Io(e.to_string()))
}
fn load_delivery_map(
path: &Path,
) -> Result<HashMap<ChannelKey, VecDeque<Vec<u8>>>, StorageError> {
/// Load deliveries as V3. Falls back to V2 format (assigns seqs starting at 0).
fn load_delivery_map_v3(path: &Path) -> Result<QueueMapV3, StorageError> {
if !path.exists() {
return Ok(HashMap::new());
return Ok(QueueMapV3::default());
}
let bytes = fs::read(path).map_err(|e| StorageError::Io(e.to_string()))?;
if bytes.is_empty() {
return Ok(HashMap::new());
return Ok(QueueMapV3::default());
}
bincode::deserialize::<QueueMapV2>(&bytes)
.map(|v| v.map)
.map_err(|_| StorageError::Io("deliveries file: v1 format no longer supported; delete or migrate".into()))
// Try V3 first.
if let Ok(v3) = bincode::deserialize::<QueueMapV3>(&bytes) {
return Ok(v3);
}
// Fall back to V2: assign ascending seqs starting at 0 per channel.
let v2 = bincode::deserialize::<QueueMapV2>(&bytes)
.map_err(|_| StorageError::Io("deliveries file: unrecognised format".into()))?;
let mut v3 = QueueMapV3::default();
for (key, queue) in v2.map {
let entries: VecDeque<SeqEntry> = queue
.into_iter()
.enumerate()
.map(|(i, data)| SeqEntry { seq: i as u64, data })
.collect();
let next = entries.len() as u64;
v3.next_seq.insert(key.clone(), next);
v3.map.insert(key, entries);
}
Ok(v3)
}
fn flush_delivery_map(
&self,
path: &Path,
map: &HashMap<ChannelKey, VecDeque<Vec<u8>>>,
) -> Result<(), StorageError> {
let payload = QueueMapV2 { map: map.clone() };
let bytes = bincode::serialize(&payload).map_err(|_| StorageError::Serde)?;
fn flush_delivery_map(&self, path: &Path, map: &QueueMapV3) -> Result<(), StorageError> {
let bytes = bincode::serialize(map).map_err(|_| StorageError::Serde)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
@@ -309,27 +340,35 @@ impl Store for FileBackedStore {
recipient_key: &[u8],
channel_id: &[u8],
payload: Vec<u8>,
) -> Result<(), StorageError> {
let mut map = lock(&self.deliveries)?;
) -> Result<u64, StorageError> {
let mut inner = lock(&self.deliveries)?;
let key = ChannelKey {
channel_id: channel_id.to_vec(),
recipient_key: recipient_key.to_vec(),
};
map.entry(key).or_default().push_back(payload);
self.flush_delivery_map(&self.ds_path, &*map)
let seq = *inner.next_seq.entry(key.clone()).or_insert(0);
*inner.next_seq.get_mut(&key).unwrap() = seq + 1;
inner.map.entry(key).or_default().push_back(SeqEntry { seq, data: payload });
self.flush_delivery_map(&self.ds_path, &*inner)?;
Ok(seq)
}
fn fetch(&self, recipient_key: &[u8], channel_id: &[u8]) -> Result<Vec<Vec<u8>>, StorageError> {
let mut map = lock(&self.deliveries)?;
fn fetch(
&self,
recipient_key: &[u8],
channel_id: &[u8],
) -> Result<Vec<(u64, Vec<u8>)>, StorageError> {
let mut inner = lock(&self.deliveries)?;
let key = ChannelKey {
channel_id: channel_id.to_vec(),
recipient_key: recipient_key.to_vec(),
};
let messages = map
let messages: Vec<(u64, Vec<u8>)> = inner
.map
.get_mut(&key)
.map(|q| q.drain(..).collect())
.map(|q| q.drain(..).map(|e| (e.seq, e.data)).collect())
.unwrap_or_default();
self.flush_delivery_map(&self.ds_path, &*map)?;
self.flush_delivery_map(&self.ds_path, &*inner)?;
Ok(messages)
}
@@ -338,30 +377,31 @@ impl Store for FileBackedStore {
recipient_key: &[u8],
channel_id: &[u8],
limit: usize,
) -> Result<Vec<Vec<u8>>, StorageError> {
let mut map = lock(&self.deliveries)?;
) -> Result<Vec<(u64, Vec<u8>)>, StorageError> {
let mut inner = lock(&self.deliveries)?;
let key = ChannelKey {
channel_id: channel_id.to_vec(),
recipient_key: recipient_key.to_vec(),
};
let messages = map
let messages: Vec<(u64, Vec<u8>)> = inner
.map
.get_mut(&key)
.map(|q| {
let count = limit.min(q.len());
q.drain(..count).collect()
q.drain(..count).map(|e| (e.seq, e.data)).collect()
})
.unwrap_or_default();
self.flush_delivery_map(&self.ds_path, &*map)?;
self.flush_delivery_map(&self.ds_path, &*inner)?;
Ok(messages)
}
fn queue_depth(&self, recipient_key: &[u8], channel_id: &[u8]) -> Result<usize, StorageError> {
let map = lock(&self.deliveries)?;
let inner = lock(&self.deliveries)?;
let key = ChannelKey {
channel_id: channel_id.to_vec(),
recipient_key: recipient_key.to_vec(),
};
Ok(map.get(&key).map(|q| q.len()).unwrap_or(0))
Ok(inner.map.get(&key).map(|q| q.len()).unwrap_or(0))
}
fn gc_expired_messages(&self, _max_age_secs: u64) -> Result<usize, StorageError> {