Skip to main content

mail4agent_server/
f3.rs

1//! F3 (feature `f3-hash-ids`, off by default): rooms created while the feature is on are DAG rooms.
2//! Every event in them (state, membership, messages) is a signed room-version-11 event with
3//! `prev_events`, `auth_events`, `depth`, a content hash, this server's ed25519 signature and an id
4//! that is the reference hash of the signed event. Legacy rooms are never touched.
5//!
6//! The rules live in `m4a-matrix-core` (storage-agnostic). This file is the store side:
7//! * [`ConnDag`] answers the core's `DagRead` from the `dag_*` tables (`dag_schema.rs`);
8//! * [`prepare_local`] is called from the two store write functions: it builds, checks, signs and
9//!   stores the event inside the caller's transaction, so the DAG rows and the event/state
10//!   projection commit or roll back together;
11//! * [`receive_pdu`] takes an event from a peer (signature, hash, auth rules, soft-fail, forks
12//!   merged by state resolution v2) and projects it in one transaction;
13//! * [`import_snapshot`] starts a replica from a peer's state (invite, send_join);
14//! * [`skeletonize`] erases the content of a delivered event and keeps its id and signatures.
15//!
16//! Not done here: redactions and public-channel (`pub_events`) messages keep legacy ids; gaps in
17//! `prev_events` are refused rather than back-filled.
18
19use std::collections::HashSet;
20
21use ed25519_dalek::Signer as _;
22use m4a_matrix_core::{dag, CanonicalJsonObject, CanonicalJsonValue, EventId, StateEventType, OwnedEventId, OwnedRoomId, Pdu, PublicKeyMap, RoomVersionRules, Signer, StateMap};
23use rusqlite::{params, Connection, OptionalExtension, Transaction};
24use serde_json::Value;
25
26use crate::error::MatrixError;
27use crate::store::MatrixStoreError;
28
29pub fn rules() -> RoomVersionRules {
30    m4a_matrix_core::ROOM_VERSION.rules().expect("room version 11 rules")
31}
32
33fn rejected(r: dag::Reject) -> MatrixStoreError {
34    MatrixStoreError::F3Rejected(r.to_string())
35}
36
37pub fn is_f3_room(conn: &Connection, room_id: &str) -> bool {
38    conn.query_row("SELECT 1 FROM dag_rooms WHERE room_id = ?1", [room_id], |_| Ok(())).optional().ok().flatten().is_some()
39}
40
41pub fn mark_room(conn: &Connection, room_id: &str) -> rusqlite::Result<()> {
42    conn.execute("INSERT OR IGNORE INTO dag_rooms (room_id, room_version) VALUES (?1, '11')", [room_id])?;
43    Ok(())
44}
45
46/// A wire event of the DAG layer: hashed, no `event_id` of its own.
47pub fn is_f3_wire(pdu: &Value) -> bool {
48    pdu.get("event_id").is_none() && pdu.get("hashes").is_some() && pdu.get("prev_events").is_some()
49}
50
51fn json_obj(v: &Value) -> Result<CanonicalJsonObject, String> {
52    serde_json::from_value(v.clone()).map_err(|e| e.to_string())
53}
54
55/// The DAG of one room as the core sees it.
56pub struct ConnDag<'a> {
57    pub conn: &'a Connection,
58    pub room: &'a str,
59}
60
61impl dag::DagRead for ConnDag<'_> {
62    fn pdu(&self, id: &EventId) -> Option<Pdu> {
63        let s: String = self.conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id.as_str()], |r| r.get(0)).optional().ok()??;
64        let obj: CanonicalJsonObject = serde_json::from_str(&s).ok()?;
65        Pdu::from_wire(id.to_owned(), &obj).ok()
66    }
67    fn state_after(&self, id: &EventId) -> Option<StateMap<OwnedEventId>> {
68        let gid: i64 = self.conn.query_row("SELECT group_id FROM dag_event_state WHERE event_id = ?1", [id.as_str()], |r| r.get(0)).optional().ok()??;
69        let mut stmt = self.conn.prepare("SELECT event_type, state_key, event_id FROM dag_state_group_entries WHERE group_id = ?1").ok()?;
70        let rows = stmt.query_map([gid], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?))).ok()?;
71        let mut m = StateMap::new();
72        for row in rows {
73            let (t, k, e) = row.ok()?;
74            m.insert((StateEventType::from(t), k), OwnedEventId::try_from(e).ok()?);
75        }
76        Some(m)
77    }
78    fn extremities(&self) -> Vec<OwnedEventId> {
79        let Ok(mut stmt) = self.conn.prepare("SELECT event_id FROM dag_extremities WHERE room_id = ?1 ORDER BY event_id") else { return vec![] };
80        let Ok(rows) = stmt.query_map([self.room], |r| r.get::<_, String>(0)) else { return vec![] };
81        rows.filter_map(|r| r.ok()).filter_map(|e| OwnedEventId::try_from(e).ok()).collect()
82    }
83}
84
85fn new_group(conn: &Connection, room: &str, state: &StateMap<OwnedEventId>) -> rusqlite::Result<i64> {
86    conn.execute("INSERT INTO dag_state_groups (room_id) VALUES (?1)", [room])?;
87    let gid = conn.last_insert_rowid();
88    for ((t, k), e) in state {
89        conn.execute("INSERT INTO dag_state_group_entries (group_id, event_type, state_key, event_id) VALUES (?1, ?2, ?3, ?4)", params![gid, t.to_string(), k, e.as_str()])?;
90    }
91    Ok(gid)
92}
93
94fn group_of(conn: &Connection, event_id: &str) -> Option<i64> {
95    conn.query_row("SELECT group_id FROM dag_event_state WHERE event_id = ?1", [event_id], |r| r.get(0)).optional().ok().flatten()
96}
97
98/// Writes one accepted event: the row, its edges, its state group, and (unless soft-failed or an
99/// outlier) the new forward extremities. The wire form is also kept in `fed_pdus`, which is what
100/// the m4a federation export serves.
101fn store_accepted(conn: &Connection, room: &str, acc: &dag::Accepted, outlier: bool) -> rusqlite::Result<()> {
102    let pdu_json = serde_json::to_string(&acc.json).unwrap_or_default();
103    conn.execute(
104        "INSERT INTO dag_events (event_id, room_id, depth, event_type, sender, state_key, origin_server_ts, pdu, outlier, soft_failed)
105         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
106        params![
107            acc.event_id.as_str(),
108            room,
109            acc.pdu.depth as i64,
110            acc.pdu.kind.to_string(),
111            acc.pdu.sender.as_str(),
112            acc.pdu.state_key,
113            u64::from(acc.pdu.origin_server_ts.0) as i64,
114            pdu_json,
115            outlier as i64,
116            acc.soft_failed as i64
117        ],
118    )?;
119    for p in &acc.pdu.prev_events {
120        conn.execute("INSERT OR IGNORE INTO dag_edges (event_id, prev_event_id) VALUES (?1, ?2)", params![acc.event_id.as_str(), p.as_str()])?;
121    }
122    for a in &acc.pdu.auth_events {
123        conn.execute("INSERT OR IGNORE INTO dag_auth (event_id, auth_event_id) VALUES (?1, ?2)", params![acc.event_id.as_str(), a.as_str()])?;
124    }
125    let shared = if acc.pdu.state_key.is_none() && acc.pdu.prev_events.len() == 1 { group_of(conn, acc.pdu.prev_events[0].as_str()) } else { None };
126    let gid = match shared {
127        Some(g) => g,
128        None => new_group(conn, room, &acc.state_after)?,
129    };
130    conn.execute("INSERT OR REPLACE INTO dag_event_state (event_id, group_id) VALUES (?1, ?2)", params![acc.event_id.as_str(), gid])?;
131    if !acc.soft_failed && !outlier {
132        conn.execute("DELETE FROM dag_extremities WHERE room_id = ?1", [room])?;
133        for e in &acc.extremities {
134            conn.execute("INSERT INTO dag_extremities (room_id, event_id) VALUES (?1, ?2)", params![room, e.as_str()])?;
135        }
136    }
137    conn.execute("INSERT OR REPLACE INTO fed_pdus (event_id, room_id, pdu) VALUES (?1, ?2, ?3)", params![acc.event_id.as_str(), room, pdu_json])?;
138    Ok(())
139}
140
141/// The id and content a local event ended up with.
142pub struct Prepared {
143    pub event_id: String,
144    pub content: String,
145}
146
147fn local_signer(conn: &Connection, now_ms: i64) -> Result<Signer, MatrixStoreError> {
148    let (key_id, key) = crate::federation::active_signing_key(conn, now_ms).map_err(|e| MatrixStoreError::F3Rejected(e.to_string()))?;
149    Ok(Signer::new(crate::store::matrix_server_name(), key_id, move |m| key.sign(m).to_bytes()))
150}
151
152/// Public key (standard base64) of one of our own signing keys, active or retired.
153pub fn own_public_key(conn: &Connection, key_id: &str) -> Option<String> {
154    let secret: Vec<u8> = conn.query_row("SELECT secret FROM fed_signing_keys WHERE key_id = ?1", [key_id], |r| r.get(0)).optional().ok()??;
155    let bytes: [u8; 32] = secret.try_into().ok()?;
156    let pk = ed25519_dalek::SigningKey::from_bytes(&bytes).verifying_key().to_bytes();
157    Some(m4a_matrix_core::key_to_b64(&pk))
158}
159
160/// Called from the store's two write functions for every event a local user creates. `None`: the
161/// room is legacy, the caller keeps its own id. `Some`: the event was built, checked against the
162/// room's auth rules, signed and stored in `tx`; the caller writes its projection with this id.
163pub fn prepare_local(tx: &Transaction, room_id: &str, sender_user_id: i64, kind: &str, state_key: Option<&str>, content: &str, ts: i64) -> Result<Option<Prepared>, MatrixStoreError> {
164    if !is_f3_room(tx, room_id) {
165        return Ok(None);
166    }
167    let sender = crate::store::mxid_of(tx, sender_user_id)?.ok_or_else(|| MatrixStoreError::UnknownMxid(sender_user_id.to_string()))?;
168    let mut c: Value = serde_json::from_str(content)?;
169    if kind == "m.room.create" && c.get("creator").is_none() {
170        // Room version 11 still names the creator in the create content.
171        c["creator"] = Value::String(sender.clone());
172    }
173    if kind == "m.room.member" && state_key == Some(sender.as_str()) && c.get("membership").and_then(Value::as_str) == Some("join") && c.get("join_authorised_via_users_server").is_none() {
174        authorise_restricted_join(tx, room_id, &sender, &mut c)?;
175    }
176    let signer = local_signer(tx, ts)?;
177    let room: OwnedRoomId = room_id.try_into().map_err(|e: m4a_matrix_core::IdError| MatrixStoreError::F3Rejected(e.to_string()))?;
178    let db = ConnDag { conn: tx, room: room_id };
179    let acc = dag::local_event(&rules(), &db, &signer, &room, ts.max(0) as u64, &sender, kind, state_key, c.clone()).map_err(rejected)?;
180    store_accepted(tx, room_id, &acc, false)?;
181    Ok(Some(Prepared { event_id: acc.event_id.to_string(), content: c.to_string() }))
182}
183
184/// A join into a restricted room by someone who is neither invited nor a member must name a local
185/// member who may invite (`join_authorised_via_users_server`): the room version's rule for
186/// space-member joins. The room creator is used when still joined, else any joined local member.
187fn authorise_restricted_join(tx: &Transaction, room_id: &str, sender: &str, content: &mut Value) -> Result<(), MatrixStoreError> {
188    let jr: Option<String> = tx
189        .query_row("SELECT e.content FROM current_state s JOIN events e ON e.event_id = s.event_id WHERE s.room_id = ?1 AND s.event_type = 'm.room.join_rules' AND s.state_key = ''", [room_id], |r| r.get(0))
190        .optional()?;
191    let restricted = jr.and_then(|c| serde_json::from_str::<Value>(&c).ok()).and_then(|v| v.get("join_rule").and_then(Value::as_str).map(|s| s == "restricted")).unwrap_or(false);
192    if !restricted {
193        return Ok(());
194    }
195    let already: Option<String> = tx.query_row("SELECT m.membership FROM room_members m JOIN matrix_users u ON u.user_id = m.user_id WHERE m.room_id = ?1 AND u.mxid = ?2", params![room_id, sender], |r| r.get(0)).optional()?;
196    if matches!(already.as_deref(), Some("invite") | Some("join")) {
197        return Ok(());
198    }
199    let via: Option<String> = tx
200        .query_row(
201            "SELECT u.mxid FROM room_members m JOIN matrix_users u ON u.user_id = m.user_id JOIN rooms r ON r.id = m.room_id
202              WHERE m.room_id = ?1 AND m.membership = 'join' AND u.user_id > 0 ORDER BY (u.user_id = r.creator_user_id) DESC, u.user_id LIMIT 1",
203            [room_id],
204            |r| r.get(0),
205        )
206        .optional()?;
207    if let Some(v) = via {
208        content["join_authorised_via_users_server"] = Value::String(v);
209    }
210    Ok(())
211}
212
213/// What [`receive_pdu`] did.
214#[derive(Debug, Default)]
215pub struct Received {
216    pub event_id: String,
217    pub duplicate: bool,
218    pub soft_failed: bool,
219    /// More than one forward extremity after this event: branches are open, state was resolved.
220    pub forked: bool,
221    pub wake: HashSet<i64>,
222}
223
224fn bad(e: impl std::fmt::Display) -> MatrixError {
225    MatrixError::forbidden(e.to_string())
226}
227
228fn str_of<'a>(v: &'a Value, k: &str) -> Result<&'a str, MatrixError> {
229    v.get(k).and_then(Value::as_str).ok_or_else(|| MatrixError::bad_json(k.to_string()))
230}
231
232fn ensure_users(conn: &Connection, pdu: &Value, now: &str) -> Result<i64, MatrixError> {
233    let sender = str_of(pdu, "sender")?;
234    // Events of this server's own users show up in a snapshot the peer sends back.
235    let uid = if crate::fed_rooms::is_remote_mxid(sender) {
236        crate::fed_rooms::ensure_remote_user(conn, sender, now).map_err(|_| MatrixError::bad_json("sender"))?
237    } else {
238        crate::store::user_id_of(conn, sender)?.ok_or_else(|| MatrixError::not_found("unknown local user"))?
239    };
240    if pdu.get("type").and_then(Value::as_str) == Some("m.room.member") {
241        let sk = str_of(pdu, "state_key")?;
242        if crate::fed_rooms::is_remote_mxid(sk) {
243            crate::fed_rooms::ensure_remote_user(conn, sk, now).map_err(|_| MatrixError::bad_json("state_key"))?;
244        } else if crate::store::user_id_of(conn, sk)?.is_none() {
245            return Err(MatrixError::not_found("unknown local user"));
246        }
247    }
248    Ok(uid)
249}
250
251/// Writes the room-visible rows (timeline row, current state, caches) of an accepted event.
252fn project(tx: &Transaction, room: &str, acc: &dag::Accepted, prev_current: &StateMap<OwnedEventId>, sender_uid: i64, now: &str) -> Result<(), MatrixStoreError> {
253    let content = acc.pdu.content.get().to_string();
254    let ts = u64::from(acc.pdu.origin_server_ts.0) as i64;
255    let id = acc.event_id.as_str();
256    let kind = acc.pdu.kind.to_string();
257    match &acc.pdu.state_key {
258        Some(sk) => {
259            crate::store::apply_state_event_raw_in_tx(tx, id, room, sender_uid, &kind, sk, &content, ts, now)?;
260            // Resolution may have picked another winner for this slot, or flipped other slots.
261            let mut slots: HashSet<(StateEventType, String)> = HashSet::new();
262            slots.insert((StateEventType::from(kind.clone()), sk.clone()));
263            for (k, v) in &acc.current_state {
264                if prev_current.get(k) != Some(v) {
265                    slots.insert(k.clone());
266                }
267            }
268            for k in slots {
269                match acc.current_state.get(&k) {
270                    Some(w) => {
271                        let have: Option<String> = tx.query_row("SELECT event_id FROM current_state WHERE room_id = ?1 AND event_type = ?2 AND state_key = ?3", params![room, k.0.to_string(), k.1], |r| r.get(0)).optional()?;
272                        if have.as_deref() != Some(w.as_str()) {
273                            crate::store::set_current_state_slot(tx, room, &k.0.to_string(), &k.1, w.as_str(), now)?;
274                        }
275                    }
276                    None => {}
277                }
278            }
279        }
280        None => {
281            let row = crate::store::TimelineEventRow { event_id: id, room_id: room, sender_user_id: sender_uid, event_type: &kind, content: &content, origin_server_ts: ts, txn_id: None };
282            crate::store::insert_timeline_event_raw_in_tx(tx, &row)?;
283        }
284    }
285    Ok(())
286}
287
288/// Why a peer's event was not taken.
289#[derive(Debug)]
290pub enum RecvErr {
291    /// Events this event needs (prev or auth) are unknown here; fetch them from the peer and retry.
292    Missing(Vec<String>),
293    Other(MatrixError),
294}
295
296impl From<MatrixError> for RecvErr {
297    fn from(e: MatrixError) -> Self {
298        RecvErr::Other(e)
299    }
300}
301
302fn is_known(conn: &Connection, id: &str) -> bool {
303    conn.query_row(
304        "SELECT 1 FROM dag_events e WHERE e.event_id = ?1 AND (e.outlier = 0 OR EXISTS (SELECT 1 FROM dag_event_state s WHERE s.event_id = e.event_id))",
305        [id],
306        |_| Ok(()),
307    )
308    .optional()
309    .ok()
310    .flatten()
311    .is_some()
312}
313
314/// A peer's event for a DAG room, sent by `origin` itself (the sender must be its user). `keys`
315/// must hold the public key of every server that signed it (the caller fetched them). Everything
316/// lands in one transaction.
317pub fn receive_pdu(conn: &mut Connection, origin: &str, pdu: &Value, keys: &PublicKeyMap, now: &str) -> Result<Received, MatrixError> {
318    receive_detail(conn, Some(origin), pdu, keys, now).map_err(|e| match e {
319        RecvErr::Missing(ids) => MatrixError::forbidden(format!("unknown prev or auth events: {}", ids.join(","))),
320        RecvErr::Other(e) => e,
321    })
322}
323
324/// [`receive_pdu`] that says when events are missing. `origin: None` is for events relayed by
325/// another server (fetched with get_missing_events): the sender may be on any server.
326pub fn receive_detail(conn: &mut Connection, origin: Option<&str>, pdu: &Value, keys: &PublicKeyMap, now: &str) -> Result<Received, RecvErr> {
327    let room = str_of(pdu, "room_id")?.to_string();
328    let sender = str_of(pdu, "sender")?.to_string();
329    if let Some(origin) = origin {
330        if crate::fed_rooms::domain_of(&sender) != Some(origin) || !crate::fed_rooms::is_remote_mxid(&sender) {
331            return Err(MatrixError::forbidden("sender is not a user of the sending server").into());
332        }
333    }
334    if !is_f3_room(conn, &room) {
335        return Err(MatrixError::not_found("unknown room").into());
336    }
337    let obj = json_obj(pdu).map_err(MatrixError::bad_json)?;
338    let rules = rules();
339    let id = dag::compute_event_id(&rules, &obj).map_err(bad)?;
340    let tx = conn.transaction().map_err(|_| MatrixError::internal())?;
341    if is_known(&tx, id.as_str()) {
342        return Ok(Received { event_id: id.to_string(), duplicate: true, ..Default::default() });
343    }
344    // Needed events first, so a gap is reported before any signature work.
345    let wire = Pdu::from_wire(id.clone(), &obj).map_err(MatrixError::bad_json)?;
346    let missing: Vec<String> = wire.prev_events.iter().chain(wire.auth_events.iter()).filter(|e| !is_known(&tx, e.as_str()) && !(wire.auth_events.contains(e) && ConnDag { conn: &tx, room: &room }.pdu_exists(e))).map(|e| e.to_string()).collect();
347    if !missing.is_empty() {
348        return Err(RecvErr::Missing(missing));
349    }
350    let sender_uid = ensure_users(&tx, pdu, now)?;
351    let db = ConnDag { conn: &tx, room: &room };
352    let prev_current = dag::current_state(&rules, &db).map_err(bad)?;
353    let acc = match dag::accept_wire(&rules, &db, obj, keys, None) {
354        Ok(a) => a,
355        Err(dag::Reject::MissingPrev(e)) | Err(dag::Reject::MissingAuth(e)) => return Err(RecvErr::Missing(vec![e])),
356        Err(e) => return Err(bad(e).into()),
357    };
358    store_accepted(&tx, &room, &acc, false).map_err(|_| MatrixError::internal())?;
359    if dag::is_skeleton(&rules, &acc.json) {
360        let _ = tx.execute("UPDATE dag_events SET skeleton = 1 WHERE event_id = ?1", [acc.event_id.as_str()]);
361    }
362    if !acc.soft_failed {
363        project(&tx, &room, &acc, &prev_current, sender_uid, now).map_err(MatrixError::from)?;
364    }
365    let wake = crate::rooms::member_and_invited_ids(&tx, &room).map_err(|_| MatrixError::internal())?;
366    tx.commit().map_err(|_| MatrixError::internal())?;
367    Ok(Received { event_id: id.to_string(), duplicate: false, soft_failed: acc.soft_failed, forked: acc.forked, wake })
368}
369
370impl ConnDag<'_> {
371    fn pdu_exists(&self, id: &EventId) -> bool {
372        dag::DagRead::pdu(self, id).is_some()
373    }
374}
375
376/// A DAG room is open to anyone when its current `m.room.join_rules` state says `public`. The rule
377/// is state (the rooms table's column is fixed at creation); this is how a closed-kind room is
378/// opened to a federated user without an invite.
379pub fn state_join_rule_is_public(conn: &Connection, room: &str) -> bool {
380    conn.query_row(
381        "SELECT e.content FROM current_state cs JOIN events e ON e.event_id = cs.event_id WHERE cs.room_id = ?1 AND cs.event_type = 'm.room.join_rules' AND cs.state_key = ''",
382        [room],
383        |r| r.get::<_, String>(0),
384    )
385    .ok()
386    .and_then(|c| serde_json::from_str::<Value>(&c).ok())
387    .is_some_and(|v| v.get("join_rule").and_then(Value::as_str) == Some("public"))
388}
389
390/// Events of the past from a peer (get_missing_events, backfill, an auth chain): each is checked
391/// (signature, hash or skeleton form, id, its own auth events, the state before it), stored as a
392/// non-extremity, and its timeline row is written when this server has none. They never change the
393/// current state or the forward extremities; the live event that needed them does that.
394pub fn process_historic(conn: &mut Connection, room: &str, events: &[Value], keys: &PublicKeyMap, now: &str) -> Result<usize, MatrixError> {
395    if !is_f3_room(conn, room) {
396        return Err(MatrixError::not_found("unknown room"));
397    }
398    let rules = rules();
399    let mut parsed: Vec<(OwnedEventId, CanonicalJsonObject, Value)> = Vec::new();
400    for v in events {
401        if v.get("room_id").and_then(Value::as_str) != Some(room) {
402            return Err(MatrixError::bad_json("event of another room"));
403        }
404        let obj = json_obj(v).map_err(MatrixError::bad_json)?;
405        dag::verify_wire(&rules, &obj, keys).map_err(bad)?;
406        let id = dag::compute_event_id(&rules, &obj).map_err(bad)?;
407        parsed.push((id, obj, v.clone()));
408    }
409    parsed.sort_by_key(|(id, o, _)| (match o.get("depth") { Some(CanonicalJsonValue::Integer(i)) => i64::from(*i), _ => 0 }, id.clone()));
410    let tx = conn.transaction().map_err(|_| MatrixError::internal())?;
411    let mut done = 0;
412    for (id, obj, v) in parsed {
413        let has_state = tx.query_row("SELECT 1 FROM dag_event_state WHERE event_id = ?1", [id.as_str()], |_| Ok(())).optional().map_err(|_| MatrixError::internal())?.is_some();
414        if has_state {
415            continue;
416        }
417        let uid = ensure_users(&tx, &v, now)?;
418        let db = ConnDag { conn: &tx, room };
419        // The event itself may already be stored as an outlier; accept_historic only needs its ancestors.
420        let h = dag::accept_historic(&rules, &db, id.clone(), obj.clone()).map_err(bad)?;
421        let text = serde_json::to_string(&obj).unwrap_or_default();
422        let skel = dag::is_skeleton(&rules, &obj);
423        tx.execute(
424            "INSERT OR IGNORE INTO dag_events (event_id, room_id, depth, event_type, sender, state_key, origin_server_ts, pdu, outlier, skeleton) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1, ?9)",
425            params![id.as_str(), room, h.pdu.depth as i64, h.pdu.kind.to_string(), h.pdu.sender.as_str(), h.pdu.state_key, u64::from(h.pdu.origin_server_ts.0) as i64, text, skel as i64],
426        )
427        .map_err(|_| MatrixError::internal())?;
428        for p in &h.pdu.prev_events {
429            let _ = tx.execute("INSERT OR IGNORE INTO dag_edges (event_id, prev_event_id) VALUES (?1, ?2)", params![id.as_str(), p.as_str()]);
430        }
431        for a in &h.pdu.auth_events {
432            let _ = tx.execute("INSERT OR IGNORE INTO dag_auth (event_id, auth_event_id) VALUES (?1, ?2)", params![id.as_str(), a.as_str()]);
433        }
434        let gid = new_group(&tx, room, &h.state_after).map_err(|_| MatrixError::internal())?;
435        tx.execute("INSERT OR REPLACE INTO dag_event_state (event_id, group_id) VALUES (?1, ?2)", params![id.as_str(), gid]).map_err(|_| MatrixError::internal())?;
436        let _ = tx.execute("INSERT OR REPLACE INTO fed_pdus (event_id, room_id, pdu) VALUES (?1, ?2, ?3)", params![id.as_str(), room, text]);
437        if skel {
438            let _ = tx.execute("INSERT OR IGNORE INTO fed_skeleton (event_id) VALUES (?1)", [id.as_str()]);
439        }
440        let in_events = tx.query_row("SELECT 1 FROM events WHERE event_id = ?1", [id.as_str()], |_| Ok(())).optional().map_err(|_| MatrixError::internal())?.is_some();
441        if !in_events {
442            let content = h.pdu.content.get().to_string();
443            let ts = u64::from(h.pdu.origin_server_ts.0) as i64;
444            let kind = h.pdu.kind.to_string();
445            match &h.pdu.state_key {
446                Some(sk) => crate::store::insert_past_state_row(&tx, id.as_str(), room, uid, &kind, sk, &content, ts).map_err(MatrixError::from)?,
447                None => {
448                    let row = crate::store::TimelineEventRow { event_id: id.as_str(), room_id: room, sender_user_id: uid, event_type: &kind, content: &content, origin_server_ts: ts, txn_id: None };
449                    crate::store::insert_timeline_event_raw_in_tx(&tx, &row).map_err(MatrixError::from)?;
450                }
451            }
452        }
453        done += 1;
454    }
455    tx.commit().map_err(|_| MatrixError::internal())?;
456    Ok(done)
457}
458
459fn pdu_value(conn: &Connection, id: &str) -> Option<Value> {
460    conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id], |r| r.get::<_, String>(0)).ok().and_then(|s| serde_json::from_str(&s).ok())
461}
462
463/// Lowest depth `origin` may be served: everything when the room's history is shared, otherwise
464/// from the first membership event of one of its users.
465fn serve_min_depth(conn: &Connection, room: &str, origin: &str) -> i64 {
466    let shared = crate::store::get_room(conn, room).ok().flatten().is_some_and(|r| matches!(r.history_visibility, crate::store::HistoryVisibility::Shared | crate::store::HistoryVisibility::WorldReadable));
467    if shared {
468        return 0;
469    }
470    conn.query_row("SELECT MIN(depth) FROM dag_events WHERE room_id = ?1 AND event_type = 'm.room.member' AND state_key LIKE ?2", params![room, format!("@%:{origin}")], |r| r.get::<_, Option<i64>>(0)).ok().flatten().unwrap_or(i64::MAX)
471}
472
473/// Walk back through `prev_events` from `roots`, nearest first, never past `stop`, at most `limit`
474/// events at or above `min_depth`. Returns `(depth, id)`.
475fn walk_back(conn: &Connection, roots: Vec<String>, stop: &HashSet<String>, limit: usize, min_depth: i64, room: &str) -> Vec<(i64, String)> {
476    let mut seen: HashSet<String> = HashSet::new();
477    let mut queue: std::collections::VecDeque<String> = roots.into();
478    let mut out = Vec::new();
479    while let Some(id) = queue.pop_front() {
480        if out.len() >= limit {
481            break;
482        }
483        if stop.contains(&id) || !seen.insert(id.clone()) {
484            continue;
485        }
486        let Some(depth): Option<i64> = conn.query_row("SELECT depth FROM dag_events WHERE event_id = ?1 AND room_id = ?2", params![id, room], |r| r.get(0)).optional().ok().flatten() else { continue };
487        if depth < min_depth {
488            continue;
489        }
490        out.push((depth, id.clone()));
491        if let Ok(mut st) = conn.prepare("SELECT prev_event_id FROM dag_edges WHERE event_id = ?1") {
492            if let Ok(rows) = st.query_map([&id], |r| r.get::<_, String>(0)) {
493                queue.extend(rows.flatten());
494            }
495        }
496    }
497    out
498}
499
500/// Whether `origin` has a user in the room (invited, joined or left): the condition for serving it history.
501pub fn origin_in_room(conn: &Connection, room: &str, origin: &str) -> bool {
502    conn.query_row("SELECT 1 FROM dag_events WHERE room_id = ?1 AND event_type = 'm.room.member' AND state_key LIKE ?2 LIMIT 1", params![room, format!("@%:{origin}")], |_| Ok(())).optional().ok().flatten().is_some()
503}
504
505/// `get_missing_events`: the ancestors of `latest` (not `latest` themselves) that are not at or
506/// behind `earliest`, oldest first. Skeletons are served as stored.
507pub fn missing_events_json(conn: &Connection, room: &str, origin: &str, earliest: &[String], latest: &[String], limit: usize) -> Vec<Value> {
508    let mut roots = Vec::new();
509    for l in latest {
510        if let Ok(mut st) = conn.prepare("SELECT prev_event_id FROM dag_edges WHERE event_id = ?1") {
511            if let Ok(rows) = st.query_map([l], |r| r.get::<_, String>(0)) {
512                roots.extend(rows.flatten());
513            }
514        }
515    }
516    let stop: HashSet<String> = earliest.iter().cloned().collect();
517    let mut found = walk_back(conn, roots, &stop, limit.clamp(1, 100), serve_min_depth(conn, room, origin), room);
518    found.sort();
519    found.into_iter().filter_map(|(_, id)| pdu_value(conn, &id)).collect()
520}
521
522/// `backfill`: the events `v` and their ancestors, newest first.
523pub fn backfill_json(conn: &Connection, room: &str, origin: &str, v: &[String], limit: usize) -> Vec<Value> {
524    let mut found = walk_back(conn, v.to_vec(), &HashSet::new(), limit.clamp(1, 100), serve_min_depth(conn, room, origin), room);
525    found.sort_by(|a, b| b.cmp(a));
526    found.into_iter().filter_map(|(_, id)| pdu_value(conn, &id)).collect()
527}
528
529/// Forward extremity ids of a room.
530pub fn extremity_ids(conn: &Connection, room: &str) -> Vec<String> {
531    dag::DagRead::extremities(&ConnDag { conn, room }).into_iter().map(|e| e.to_string()).collect()
532}
533
534/// Every event of a snapshot as one list: state, extremities, auth chain.
535pub fn snapshot_events(snap: &Value) -> Vec<Value> {
536    ["state", "extremities", "auth_chain"].iter().flat_map(|k| snap.get(*k).and_then(Value::as_array).cloned().unwrap_or_default()).collect()
537}
538
539/// The room as it was right after `at` (an event this server holds): the state events of that
540/// state and the event itself, as stored wire JSON. A peer that is invited or joins starts its
541/// replica from this, with `at` as its only forward extremity, so the events that follow cite
542/// something it holds.
543pub fn snapshot_json(conn: &Connection, room: &str, at: &str) -> Result<Value, MatrixError> {
544    let db = ConnDag { conn, room };
545    let id = OwnedEventId::try_from(at).map_err(|_| MatrixError::bad_json("event id"))?;
546    let after = dag::DagRead::state_after(&db, &id).ok_or_else(|| MatrixError::not_found("unknown event"))?;
547    let get = |id: &str| -> Option<Value> { conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id], |r| r.get::<_, String>(0)).ok().and_then(|s| serde_json::from_str(&s).ok()) };
548    let state: Vec<Value> = after.values().filter_map(|i| get(i.as_str())).collect();
549    let extremities: Vec<Value> = get(at).into_iter().collect();
550    // The auth chain: everything the state events (and the event itself) cite, transitively.
551    let mut have: HashSet<String> = after.values().map(|i| i.to_string()).collect();
552    have.insert(at.to_string());
553    let mut todo: Vec<String> = have.iter().cloned().collect();
554    let mut chain = Vec::new();
555    while let Some(id) = todo.pop() {
556        if let Ok(mut st) = conn.prepare("SELECT auth_event_id FROM dag_auth WHERE event_id = ?1") {
557            let ids: Vec<String> = st.query_map([&id], |r| r.get::<_, String>(0)).map(|r| r.flatten().collect()).unwrap_or_default();
558            for a in ids {
559                if have.insert(a.clone()) {
560                    if let Some(p) = get(&a) {
561                        chain.push(p);
562                    }
563                    todo.push(a);
564                }
565            }
566        }
567    }
568    Ok(serde_json::json!({ "state": state, "extremities": extremities, "auth_chain": chain }))
569}
570
571/// Auth chain (events not in `seeds` themselves) of the given events, as stored wire JSON.
572pub fn auth_chain_of(conn: &Connection, seeds: &[String]) -> Vec<Value> {
573    let get = |id: &str| -> Option<Value> { conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id], |r| r.get::<_, String>(0)).ok().and_then(|s| serde_json::from_str(&s).ok()) };
574    let mut have: HashSet<String> = seeds.iter().cloned().collect();
575    let mut todo: Vec<String> = seeds.to_vec();
576    let mut chain = Vec::new();
577    while let Some(id) = todo.pop() {
578        let ids: Vec<String> = conn
579            .prepare("SELECT auth_event_id FROM dag_auth WHERE event_id = ?1")
580            .and_then(|mut st| st.query_map([&id], |r| r.get::<_, String>(0)).map(|r| r.flatten().collect()))
581            .unwrap_or_default();
582        for a in ids {
583            if have.insert(a.clone()) {
584                if let Some(p) = get(&a) {
585                    chain.push(p);
586                }
587                todo.push(a);
588            }
589        }
590    }
591    chain
592}
593
594/// `event_auth`: the auth chain of one event.
595pub fn event_auth_json(conn: &Connection, room: &str, event_id: &str) -> Result<Value, MatrixError> {
596    let known: Option<i64> = conn.query_row("SELECT 1 FROM dag_events WHERE event_id = ?1", [event_id], |r| r.get(0)).optional().ok().flatten();
597    if known.is_none() {
598        return Err(MatrixError::not_found("unknown event"));
599    }
600    let _ = room;
601    Ok(serde_json::json!({ "auth_chain": auth_chain_of(conn, &[event_id.to_string()]) }))
602}
603
604/// `state_ids` (ids only) or `state` (events): the room's state right after `at`, and the auth chain.
605pub fn state_at_json(conn: &Connection, room: &str, at: &str, ids_only: bool) -> Result<Value, MatrixError> {
606    let db = ConnDag { conn, room };
607    let id = OwnedEventId::try_from(at).map_err(|_| MatrixError::bad_json("event id"))?;
608    let after = dag::DagRead::state_after(&db, &id).ok_or_else(|| MatrixError::not_found("unknown event"))?;
609    let ids: Vec<String> = after.values().map(|i| i.to_string()).collect();
610    let chain = auth_chain_of(conn, &ids);
611    if ids_only {
612        let chain_ids: Vec<String> = chain.iter().filter_map(|p| pdu_event_id(p)).collect();
613        return Ok(serde_json::json!({ "pdu_ids": ids, "auth_chain_ids": chain_ids }));
614    }
615    let get = |id: &str| -> Option<Value> { conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id], |r| r.get::<_, String>(0)).ok().and_then(|s| serde_json::from_str(&s).ok()) };
616    let pdus: Vec<Value> = ids.iter().filter_map(|i| get(i)).collect();
617    Ok(serde_json::json!({ "pdus": pdus, "auth_chain": chain }))
618}
619
620fn pdu_event_id(p: &Value) -> Option<String> {
621    let obj = json_obj(p).ok()?;
622    dag::compute_event_id(&rules(), &obj).ok().map(|i| i.to_string())
623}
624
625/// Start (or complete) a replica of a DAG room from a peer's snapshot: every event is checked for
626/// its signature and content hash, stored as an outlier, the state events are projected, and the
627/// snapshot's extremities become this replica's forward extremities.
628pub fn import_snapshot(conn: &mut Connection, room_id: &str, info: &Value, snap: &Value, keys: &PublicKeyMap, now: &str) -> Result<(), MatrixError> {
629    let rules = rules();
630    let list = |k: &str| snap.get(k).and_then(Value::as_array).cloned().unwrap_or_default();
631    let (state_v, ext_v) = (list("state"), list("extremities"));
632    let tx = conn.transaction().map_err(|_| MatrixError::internal())?;
633    crate::fed_rooms::create_replica_room(&tx, room_id, info, now)?;
634    mark_room(&tx, room_id).map_err(|_| MatrixError::internal())?;
635    let ext_ids: HashSet<OwnedEventId> = ext_v.iter().filter_map(|e| json_obj(e).ok()).filter_map(|o| dag::compute_event_id(&rules, &o).ok()).collect();
636    // The joiner re-verifies the whole auth chain: signatures, hashes, ids, and the auth rules of
637    // every event against the state its own auth events describe.
638    let chain_v = list("auth_chain");
639    let all_objs: Vec<CanonicalJsonObject> = state_v.iter().chain(ext_v.iter()).chain(chain_v.iter()).map(|v| json_obj(v).map_err(MatrixError::bad_json)).collect::<Result<_, _>>()?;
640    dag::verify_auth_chain(&rules, &all_objs, keys).map_err(bad)?;
641    let mut parsed: Vec<(OwnedEventId, CanonicalJsonObject, Value)> = Vec::new();
642    for v in state_v.iter().chain(ext_v.iter()).chain(chain_v.iter()) {
643        if v.get("room_id").and_then(Value::as_str) != Some(room_id) {
644            return Err(MatrixError::bad_json("event of another room"));
645        }
646        let obj = json_obj(v).map_err(MatrixError::bad_json)?;
647        dag::verify_wire(&rules, &obj, keys).map_err(bad)?;
648        let id = dag::compute_event_id(&rules, &obj).map_err(bad)?;
649        if parsed.iter().all(|(i, _, _)| *i != id) {
650            parsed.push((id, obj, v.clone()));
651        }
652    }
653    parsed.sort_by_key(|(_, o, _)| match o.get("depth") {
654        Some(CanonicalJsonValue::Integer(i)) => i64::from(*i),
655        _ => 0,
656    });
657    let mut state: StateMap<OwnedEventId> = StateMap::new();
658    let state_ids: HashSet<OwnedEventId> = state_v.iter().filter_map(|e| json_obj(e).ok()).filter_map(|o| dag::compute_event_id(&rules, &o).ok()).collect();
659    for (id, obj, v) in &parsed {
660        let pdu = Pdu::from_wire(id.clone(), obj).map_err(MatrixError::bad_json)?;
661        let uid = ensure_users(&tx, v, now)?;
662        let known = tx.query_row("SELECT 1 FROM dag_events WHERE event_id = ?1", [id.as_str()], |_| Ok(())).optional().map_err(|_| MatrixError::internal())?.is_some();
663        if !known {
664            // Stored as an outlier: its own edges are not followed, the state group below is the snapshot.
665            tx.execute(
666                "INSERT INTO dag_events (event_id, room_id, depth, event_type, sender, state_key, origin_server_ts, pdu, outlier) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1)",
667                params![id.as_str(), room_id, pdu.depth as i64, pdu.kind.to_string(), pdu.sender.as_str(), pdu.state_key, u64::from(pdu.origin_server_ts.0) as i64, serde_json::to_string(obj).unwrap_or_default()],
668            )
669            .map_err(|_| MatrixError::internal())?;
670            for p in &pdu.prev_events {
671                let _ = tx.execute("INSERT OR IGNORE INTO dag_edges (event_id, prev_event_id) VALUES (?1, ?2)", params![id.as_str(), p.as_str()]);
672            }
673            for a in &pdu.auth_events {
674                let _ = tx.execute("INSERT OR IGNORE INTO dag_auth (event_id, auth_event_id) VALUES (?1, ?2)", params![id.as_str(), a.as_str()]);
675            }
676            let _ = tx.execute("INSERT OR REPLACE INTO fed_pdus (event_id, room_id, pdu) VALUES (?1, ?2, ?3)", params![id.as_str(), room_id, serde_json::to_string(obj).unwrap_or_default()]);
677        }
678        if let Some(sk) = &pdu.state_key {
679            if state_ids.contains(id) {
680                state.insert((StateEventType::from(pdu.kind.to_string()), sk.clone()), id.clone());
681                if !known {
682                    let content = pdu.content.get().to_string();
683                    crate::store::apply_state_event_raw_in_tx(&tx, id.as_str(), room_id, uid, &pdu.kind.to_string(), sk, &content, u64::from(pdu.origin_server_ts.0) as i64, now).map_err(MatrixError::from)?;
684                }
685            }
686        }
687    }
688    // A replica that already exists (an invite came first) moves to the snapshot's state.
689    for ((t, k), id) in &state {
690        let have: Option<String> = tx.query_row("SELECT event_id FROM current_state WHERE room_id = ?1 AND event_type = ?2 AND state_key = ?3", params![room_id, t.to_string(), k], |r| r.get(0)).optional().map_err(|_| MatrixError::internal())?;
691        if have.as_deref() != Some(id.as_str()) {
692            crate::store::set_current_state_slot(&tx, room_id, &t.to_string(), k, id.as_str(), now).map_err(MatrixError::from)?;
693        }
694    }
695    // Placeholders made by a spec invite give way to the real events.
696    let _ = tx.execute("DELETE FROM current_state WHERE room_id = ?1 AND event_id LIKE '$stripped-%'", [room_id]);
697    let _ = tx.execute("DELETE FROM events WHERE room_id = ?1 AND event_id LIKE '$stripped-%'", [room_id]);
698    let gid = new_group(&tx, room_id, &state).map_err(|_| MatrixError::internal())?;
699    tx.execute("DELETE FROM dag_extremities WHERE room_id = ?1", [room_id]).map_err(|_| MatrixError::internal())?;
700    for (id, _, _) in &parsed {
701        if ext_ids.contains(id) {
702            tx.execute("INSERT OR REPLACE INTO dag_event_state (event_id, group_id) VALUES (?1, ?2)", params![id.as_str(), gid]).map_err(|_| MatrixError::internal())?;
703            tx.execute("INSERT OR IGNORE INTO dag_extremities (room_id, event_id) VALUES (?1, ?2)", params![room_id, id.as_str()]).map_err(|_| MatrixError::internal())?;
704        }
705    }
706    tx.commit().map_err(|_| MatrixError::internal())?;
707    Ok(())
708}
709
710/// An invite from a server that follows the spec (room version 11, no snapshot): the invite event
711/// is verified and stored as an outlier (it is all this server knows of the room), and the stripped
712/// state the inviter sent is projected under placeholder ids so a client can show what it is invited
713/// to. The real events replace the placeholders when the invitee joins (`import_snapshot`).
714pub fn import_spec_invite(conn: &mut Connection, room_id: &str, invite: &Value, stripped: &[Value], keys: &PublicKeyMap, now: &str) -> Result<(), MatrixError> {
715    let rules = rules();
716    let obj = json_obj(invite).map_err(MatrixError::bad_json)?;
717    dag::verify_wire(&rules, &obj, keys).map_err(bad)?;
718    let id = dag::compute_event_id(&rules, &obj).map_err(bad)?;
719    let pdu = Pdu::from_wire(id.clone(), &obj).map_err(MatrixError::bad_json)?;
720    if pdu.room_id.as_str() != room_id {
721        return Err(MatrixError::bad_json("event of another room"));
722    }
723    let has = |ty: &str| stripped.iter().any(|s| s.get("type").and_then(Value::as_str) == Some(ty));
724    let info = serde_json::json!({ "kind": "group", "join_rule": "invite", "history_visibility": "shared", "creator": pdu.sender.as_str(), "is_encrypted": has("m.room.encryption") });
725    let tx = conn.transaction().map_err(|_| MatrixError::internal())?;
726    crate::fed_rooms::create_replica_room(&tx, room_id, &info, now)?;
727    mark_room(&tx, room_id).map_err(|_| MatrixError::internal())?;
728    let known = tx.query_row("SELECT 1 FROM dag_events WHERE event_id = ?1", [id.as_str()], |_| Ok(())).optional().map_err(|_| MatrixError::internal())?.is_some();
729    if !known {
730        let uid = ensure_users(&tx, invite, now)?;
731        let ts = u64::from(pdu.origin_server_ts.0) as i64;
732        let text = serde_json::to_string(&obj).unwrap_or_default();
733        tx.execute(
734            "INSERT INTO dag_events (event_id, room_id, depth, event_type, sender, state_key, origin_server_ts, pdu, outlier) VALUES (?1, ?2, ?3, 'm.room.member', ?4, ?5, ?6, ?7, 1)",
735            params![id.as_str(), room_id, pdu.depth as i64, pdu.sender.as_str(), pdu.state_key, ts, text],
736        )
737        .map_err(|_| MatrixError::internal())?;
738        let _ = tx.execute("INSERT OR REPLACE INTO fed_pdus (event_id, room_id, pdu) VALUES (?1, ?2, ?3)", params![id.as_str(), room_id, text]);
739        for (n, s) in stripped.iter().enumerate() {
740            let (Some(ty), Some(sk)) = (s.get("type").and_then(Value::as_str), s.get("state_key").and_then(Value::as_str)) else { continue };
741            if ty == "m.room.member" {
742                continue;
743            }
744            let content = s.get("content").cloned().unwrap_or_else(|| serde_json::json!({}));
745            let ph = format!("$stripped-{n}-{}", id.as_str().trim_start_matches('$'));
746            let _ = crate::store::apply_state_event_raw_in_tx(&tx, &ph, room_id, uid, ty, sk, &content.to_string(), ts, now);
747        }
748        crate::store::apply_state_event_raw_in_tx(&tx, id.as_str(), room_id, uid, "m.room.member", pdu.state_key.as_deref().unwrap_or(""), pdu.content.get(), ts, now).map_err(MatrixError::from)?;
749        let mut state: StateMap<OwnedEventId> = StateMap::new();
750        state.insert((StateEventType::from("m.room.member".to_string()), pdu.state_key.clone().unwrap_or_default()), id.clone());
751        let gid = new_group(&tx, room_id, &state).map_err(|_| MatrixError::internal())?;
752        tx.execute("INSERT OR REPLACE INTO dag_event_state (event_id, group_id) VALUES (?1, ?2)", params![id.as_str(), gid]).map_err(|_| MatrixError::internal())?;
753        tx.execute("INSERT OR IGNORE INTO dag_extremities (room_id, event_id) VALUES (?1, ?2)", params![room_id, id.as_str()]).map_err(|_| MatrixError::internal())?;
754    }
755    tx.commit().map_err(|_| MatrixError::internal())?;
756    Ok(())
757}
758
759/// Event id of a wire event that has none of its own.
760pub fn wire_id(pdu: &Value) -> Result<String, MatrixError> {
761    let obj = json_obj(pdu).map_err(MatrixError::bad_json)?;
762    Ok(dag::compute_event_id(&rules(), &obj).map_err(bad)?.to_string())
763}
764
765/// Public keys needed to check `pdus`, for signers whose keys are our own (the caller fills in
766/// the rest). Returns the `(server, key id)` pairs that still need a key.
767pub fn missing_keys(conn: &Connection, pdus: &[&Value], keys: &mut PublicKeyMap) -> Vec<(String, String)> {
768    let mut need = Vec::new();
769    for p in pdus {
770        let Some(sigs) = p.get("signatures").and_then(Value::as_object) else { continue };
771        for (server, ks) in sigs {
772            for key_id in ks.as_object().into_iter().flat_map(|o| o.keys()) {
773                if keys.get(server).is_some_and(|m| m.contains_key(key_id)) {
774                    continue;
775                }
776                if crate::store::is_local_server_name(server) {
777                    if let Some(pk) = own_public_key(conn, key_id).and_then(|b| m4a_matrix_core::key_from_b64(&b).ok()) {
778                        keys.entry(server.clone()).or_default().insert(key_id.clone(), pk);
779                        continue;
780                    }
781                }
782                if !need.contains(&(server.clone(), key_id.clone())) {
783                    need.push((server.clone(), key_id.clone()));
784                }
785            }
786        }
787    }
788    need
789}
790
791/// Add one fetched remote key (standard base64) to `keys`.
792pub fn add_key(keys: &mut PublicKeyMap, server: &str, key_id: &str, public_key_b64: &str) -> Result<(), MatrixError> {
793    let pk = m4a_matrix_core::key_from_b64(public_key_b64).map_err(|_| MatrixError::bad_json("remote key"))?;
794    keys.entry(server.to_owned()).or_default().insert(key_id.to_owned(), pk);
795    Ok(())
796}
797
798/// Erase the content of a delivered event: the room row and the stored wire JSON become the
799/// redacted form. Its id (a hash of the redacted form), edges, hashes and signatures stay, so peers
800/// still validate it. Refuses while a federation delivery of the event is still queued.
801pub fn skeletonize(conn: &Connection, event_id: &str) -> Result<bool, MatrixStoreError> {
802    let pending: i64 = conn.query_row("SELECT COUNT(*) FROM fed_outbox WHERE event_id = ?1", [event_id], |r| r.get(0))?;
803    if pending > 0 {
804        return Ok(false);
805    }
806    let Some(s): Option<String> = conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [event_id], |r| r.get(0)).optional()? else { return Ok(false) };
807    let obj: CanonicalJsonObject = serde_json::from_str(&s)?;
808    let sk = dag::skeleton(&rules(), &obj).map_err(rejected)?;
809    let text = serde_json::to_string(&sk)?;
810    conn.execute("UPDATE dag_events SET pdu = ?2, skeleton = 1 WHERE event_id = ?1", params![event_id, text])?;
811    conn.execute("UPDATE fed_pdus SET pdu = ?2 WHERE event_id = ?1", params![event_id, text])?;
812    conn.execute("UPDATE events SET content = '{}' WHERE event_id = ?1", [event_id])?;
813    conn.execute("INSERT OR IGNORE INTO fed_skeleton (event_id) VALUES (?1)", [event_id])?;
814    Ok(true)
815}
816
817/// Template for a peer's join (make_join): an unsigned event with this room's `prev_events`,
818/// `auth_events` and `depth`.
819pub fn join_template(conn: &Connection, room_id: &str, user_mxid: &str, ts: i64) -> Result<Value, MatrixError> {
820    let room: OwnedRoomId = room_id.try_into().map_err(|_| MatrixError::bad_json("room id"))?;
821    let db = ConnDag { conn, room: room_id };
822    let t = dag::template(&rules(), &db, &room, ts.max(0) as u64, user_mxid, "m.room.member", Some(user_mxid), serde_json::json!({ "membership": "join" })).map_err(bad)?;
823    serde_json::to_value(t).map_err(|_| MatrixError::internal())
824}
825
826/// Template for a peer's leave (make_leave), like [`join_template`].
827pub fn leave_template(conn: &Connection, room_id: &str, user_mxid: &str, ts: i64) -> Result<Value, MatrixError> {
828    let room: OwnedRoomId = room_id.try_into().map_err(|_| MatrixError::bad_json("room id"))?;
829    let db = ConnDag { conn, room: room_id };
830    let t = dag::template(&rules(), &db, &room, ts.max(0) as u64, user_mxid, "m.room.member", Some(user_mxid), serde_json::json!({ "membership": "leave" })).map_err(bad)?;
831    serde_json::to_value(t).map_err(|_| MatrixError::internal())
832}
833
834/// One stored event as a wire PDU (`GET /event/{eventId}`), if it is a DAG event.
835pub fn pdu_json(conn: &Connection, event_id: &str) -> Option<Value> {
836    conn.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [event_id], |r| r.get::<_, String>(0)).ok().and_then(|s| serde_json::from_str(&s).ok())
837}
838
839/// The room a stored DAG event belongs to.
840pub fn room_of_event(conn: &Connection, event_id: &str) -> Option<String> {
841    conn.query_row("SELECT room_id FROM dag_events WHERE event_id = ?1", [event_id], |r| r.get::<_, String>(0)).ok()
842}
843
844/// Sign a filled-in template with this server's key. Returns the event id and the wire event.
845pub fn sign_own(conn: &Connection, template: &Value, now_ms: i64) -> Result<(String, Value), MatrixError> {
846    let signer = local_signer(conn, now_ms)?;
847    let obj = json_obj(template).map_err(MatrixError::bad_json)?;
848    let s = dag::sign_template(&rules(), &signer, obj).map_err(bad)?;
849    Ok((s.event_id.to_string(), serde_json::to_value(&s.json).map_err(|_| MatrixError::internal())?))
850}
851
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856    use crate::rooms::{apply_create_room, apply_invite, decide_and_apply_join, InviteTarget, RoomCreate, RoomCreation};
857    use crate::store;
858    use m4a_matrix_core::{key_to_b64, Signer};
859
860    const NOW: &str = "2026-10-10T00:00:00+00:00";
861    const TS: i64 = 1_760_000_000_000;
862
863    fn conn() -> Connection {
864        let c = Connection::open_in_memory().unwrap();
865        store::create_matrix_schema(&c).unwrap();
866        store::ensure_matrix_user(&c, 1, "alice000000000000000000000000001", NOW).unwrap();
867        store::ensure_matrix_user(&c, 2, "bob00000000000000000000000000002", NOW).unwrap();
868        c
869    }
870
871    fn create(c: &mut Connection, public: bool) -> String {
872        let mxid = store::mxid_of(c, 1).unwrap().unwrap();
873        match apply_create_room(c, RoomCreate { creator_user_id: 1, creator_mxid: &mxid, creator_displayname: "alice", is_direct: false, invitees: &[], visibility_public: public, power_level_content_override: None, name: Some("t"), topic: None, room_type: None, predecessor: None }, NOW, TS).unwrap() {
874            RoomCreation::Created { room_id, .. } => room_id,
875            RoomCreation::Reused(_) => unreachable!(),
876        }
877    }
878
879    fn ids(c: &Connection, room: &str) -> Vec<String> {
880        c.prepare("SELECT event_id FROM events WHERE room_id = ?1 ORDER BY stream_id").unwrap().query_map([room], |r| r.get(0)).unwrap().map(Result::unwrap).collect()
881    }
882
883    fn count(c: &Connection, sql: &str) -> i64 {
884        c.query_row(sql, [], |r| r.get(0)).unwrap()
885    }
886
887    fn is_hash_id(id: &str) -> bool {
888        id.starts_with('$') && id.len() == 44 && !id.contains(['+', '/', '='])
889    }
890
891    fn send(c: &mut Connection, room: &str, txn: &str, ts: i64) -> store::MatrixEvent {
892        let r = store::get_room(c, room).unwrap().unwrap();
893        let mxid = store::mxid_of(c, 1).unwrap().unwrap();
894        crate::messaging::apply_send(c, &r, 1, &mxid, "DEV", txn, &store::new_event_id(), "m.room.encrypted", r#"{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"c2VjcmV0","session_id":"s","sender_key":"k","device_id":"DEV"}"#, NOW, ts).unwrap().event
895    }
896
897    #[test]
898    fn closed_room_lifecycle_events_all_get_hash_ids_and_dag_rows() {
899        let mut c = conn();
900        let room = create(&mut c, false);
901        let alice = store::mxid_of(&c, 1).unwrap().unwrap();
902        let bob = store::mxid_of(&c, 2).unwrap().unwrap();
903        apply_invite(&mut c, &room, 1, &alice, InviteTarget { user_id: 2, displayname: "bob" }, NOW, TS + 10).unwrap();
904        decide_and_apply_join(&mut c, &room, 2, &bob, "bob", NOW, TS + 20).unwrap();
905        let m = send(&mut c, &room, "t1", TS + 30);
906        crate::rooms::apply_leave(&mut c, &room, 2, &bob, NOW, TS + 40).unwrap();
907
908        let all = ids(&c, &room);
909        assert!(all.len() >= 9, "bootstrap + invite + join + message + leave: {all:?}");
910        for id in &all {
911            assert!(is_hash_id(id), "{id}");
912            assert!(count(&c, &format!("SELECT COUNT(*) FROM dag_events WHERE event_id = '{id}' AND soft_failed = 0 AND outlier = 0")) == 1);
913            let pdu: String = c.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [id], |r| r.get(0)).unwrap();
914            let v: Value = serde_json::from_str(&pdu).unwrap();
915            assert!(v["hashes"]["sha256"].is_string() && v["signatures"][store::matrix_server_name()].is_object() && v.get("event_id").is_none());
916            assert!(v["depth"].as_u64().unwrap() >= 1);
917        }
918        assert!(is_hash_id(&m.event_id), "the id the sender gets back is the computed one");
919        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_events"), all.len() as i64);
920        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_extremities"), 1);
921        let last: String = c.query_row("SELECT event_id FROM dag_extremities", [], |r| r.get(0)).unwrap();
922        assert_eq!(&last, all.last().unwrap(), "linear history: the last event is the only extremity");
923        // The projection the clients read equals the DAG's resolved state.
924        let db = ConnDag { conn: &c, room: &room };
925        let cur = dag::current_state(&rules(), &db).unwrap();
926        let table: i64 = count(&c, &format!("SELECT COUNT(*) FROM current_state WHERE room_id = '{room}'"));
927        assert_eq!(cur.len() as i64, table);
928        for ((t, k), id) in &cur {
929            let have: String = c.query_row("SELECT event_id FROM current_state WHERE room_id = ?1 AND event_type = ?2 AND state_key = ?3", params![room, t.to_string(), k], |r| r.get(0)).unwrap();
930            assert_eq!(have, id.as_str());
931        }
932        let bob_state = store::room_member(&c, &room, 2).unwrap().unwrap().membership;
933        assert_eq!(bob_state, store::Membership::Leave);
934        // A repeated transaction id returns the same event, no second row.
935        let again = send(&mut c, &room, "t1", TS + 31);
936        assert_eq!(again.event_id, m.event_id);
937    }
938
939    #[test]
940    fn the_event_write_and_the_dag_rows_roll_back_together() {
941        let mut c = conn();
942        let room = create(&mut c, false);
943        let before = (count(&c, "SELECT COUNT(*) FROM dag_events"), count(&c, "SELECT COUNT(*) FROM events"));
944        {
945            let tx = c.transaction().unwrap();
946            let p = prepare_local(&tx, &room, 1, "m.room.message", None, r#"{"body":"x"}"#, TS + 5).unwrap().unwrap();
947            assert!(is_hash_id(&p.event_id));
948            assert_eq!(count(&tx, "SELECT COUNT(*) FROM dag_events"), before.0 + 1);
949            tx.rollback().unwrap();
950        }
951        assert_eq!((count(&c, "SELECT COUNT(*) FROM dag_events"), count(&c, "SELECT COUNT(*) FROM events")), before);
952        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_extremities"), 1);
953    }
954
955    #[test]
956    fn auth_rules_stop_an_event_in_the_same_transaction() {
957        let mut c = conn();
958        let room = create(&mut c, false);
959        let before = count(&c, "SELECT COUNT(*) FROM dag_events");
960        // bob is not a member: a state event from him is refused by the DAG's auth rules, nothing is written.
961        let tx = c.transaction().unwrap();
962        let r = prepare_local(&tx, &room, 2, "m.room.name", Some(""), r#"{"name":"x"}"#, TS + 6);
963        assert!(matches!(r, Err(MatrixStoreError::F3Rejected(_))));
964        drop(tx);
965        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_events"), before);
966    }
967
968    #[test]
969    fn legacy_rooms_and_public_channels_are_untouched() {
970        let mut c = conn();
971        let chan = create(&mut c, true);
972        assert!(!is_f3_room(&c, &chan));
973        for id in ids(&c, &chan) {
974            assert!(!is_hash_id(&id) && id.len() < 30, "legacy random id: {id}");
975        }
976        // A room that exists without being marked keeps the id its caller chose.
977        c.execute("INSERT INTO rooms (id, kind, creator_user_id, created_at) VALUES ('!old:example.org', 'group', 1, 't')", []).unwrap();
978        store::apply_state_event(&mut c, &store::StateEventWrite { event_id: "$chosen", room_id: "!old:example.org", sender_user_id: 1, event_type: "m.room.topic", state_key: "", content: r#"{"topic":"t"}"#, origin_server_ts: TS, now: NOW }).unwrap();
979        assert_eq!(ids(&c, "!old:example.org"), vec!["$chosen".to_string()]);
980        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_events WHERE room_id = '!old:example.org'"), 0);
981    }
982
983    #[test]
984    fn skeleton_erases_content_keeps_id_and_signatures() {
985        let mut c = conn();
986        let room = create(&mut c, false);
987        let m = send(&mut c, &room, "t1", TS + 30);
988        c.execute("INSERT INTO fed_outbox (destination, kind, room_id, event_id, payload, created_ms) VALUES ('b.example', 'send', ?1, ?2, '{}', 1)", params![room, m.event_id]).unwrap();
989        assert!(!skeletonize(&c, &m.event_id).unwrap(), "refused while a delivery is queued");
990        c.execute("DELETE FROM fed_outbox", []).unwrap();
991        assert!(skeletonize(&c, &m.event_id).unwrap());
992        let content: String = c.query_row("SELECT content FROM events WHERE event_id = ?1", [&m.event_id], |r| r.get(0)).unwrap();
993        assert_eq!(content, "{}");
994        let pdu: String = c.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [&m.event_id], |r| r.get(0)).unwrap();
995        let obj: CanonicalJsonObject = serde_json::from_str(&pdu).unwrap();
996        assert_eq!(dag::compute_event_id(&rules(), &obj).unwrap().as_str(), m.event_id, "the skeleton still hashes to the same id");
997        assert!(!pdu.contains("c2VjcmV0") && pdu.contains("signatures") && pdu.contains("prev_events"));
998    }
999
1000    fn recv(c: &mut Connection, origin: &str, pdu: &Value) -> Result<Received, MatrixError> {
1001        let keys = peer_keys(c, pdu);
1002        receive_pdu(c, origin, pdu, &keys, NOW)
1003    }
1004
1005    #[test]
1006    fn retention_turns_delivered_messages_of_dag_rooms_into_skeletons_instead_of_deleting_them() {
1007        let mut c = conn();
1008        crate::keys::create_matrix_keys_schema(&c).unwrap();
1009        crate::retention::create_retention_schema(&c).unwrap();
1010        let room = create(&mut c, false);
1011        let m = send(&mut c, &room, "t1", TS + 30);
1012        let policy = crate::retention::RetentionPolicy { ttl_ms: 1_000, ack_grace_ms: 0, keep_last: 0, stale_device_ms: 1 };
1013        let n = crate::retention::purge_delivered_events(&mut c, TS + 10_000_000, &policy).unwrap();
1014        assert_eq!(n, 1);
1015        // The row stays (its place in the room), the ciphertext is gone, the id still verifies.
1016        let content: String = c.query_row("SELECT content FROM events WHERE event_id = ?1", [&m.event_id], |r| r.get(0)).unwrap();
1017        assert_eq!(content, "{}");
1018        let skel: i64 = c.query_row("SELECT skeleton FROM dag_events WHERE event_id = ?1", [&m.event_id], |r| r.get(0)).unwrap();
1019        assert_eq!(skel, 1);
1020        // A second pass has nothing left to do.
1021        assert_eq!(crate::retention::purge_delivered_events(&mut c, TS + 10_000_000, &policy).unwrap(), 0);
1022    }
1023
1024    /// A second database standing in for another server that holds a replica of A's room (it shares
1025    /// A's users and name, which is enough for the DAG logic); keys come from A.
1026    fn replica_conn() -> Connection {
1027        conn()
1028    }
1029
1030    fn a_keys(a: &Connection, events: &[Value]) -> PublicKeyMap {
1031        let mut keys = PublicKeyMap::new();
1032        let refs: Vec<&Value> = events.iter().collect();
1033        assert!(missing_keys(a, &refs, &mut keys).is_empty());
1034        keys
1035    }
1036
1037    fn room_with_bob_joined(c: &mut Connection) -> String {
1038        let room = create(c, false);
1039        let alice = store::mxid_of(c, 1).unwrap().unwrap();
1040        let bob = store::mxid_of(c, 2).unwrap().unwrap();
1041        apply_invite(c, &room, 1, &alice, InviteTarget { user_id: 2, displayname: "bob" }, NOW, TS + 10).unwrap();
1042        decide_and_apply_join(c, &room, 2, &bob, "bob", NOW, TS + 20).unwrap();
1043        room
1044    }
1045
1046    #[test]
1047    fn a_joiner_reverifies_the_auth_chain_and_refuses_a_missing_or_altered_link() {
1048        let mut a = conn();
1049        let room = room_with_bob_joined(&mut a);
1050        let last = extremity_ids(&a, &room).remove(0);
1051        let snap = snapshot_json(&a, &room, &last).unwrap();
1052        let chain = snap["auth_chain"].as_array().unwrap().clone();
1053        assert!(!chain.is_empty(), "bob's invite is superseded by his join: it travels in the chain");
1054        let info = crate::fed_rooms::room_info(&a, &room).unwrap();
1055
1056        let try_import = |snap: &Value| {
1057            let mut b = replica_conn();
1058            let keys = a_keys(&a, &snapshot_events(snap));
1059            let r = import_snapshot(&mut b, &room, &info, snap, &keys, NOW);
1060            (r, b)
1061        };
1062        let (ok, b) = try_import(&snap);
1063        ok.unwrap();
1064        assert_eq!(count(&b, "SELECT COUNT(*) FROM dag_extremities"), 1);
1065        assert_eq!(store::room_member(&b, &room, 2).unwrap().unwrap().membership, store::Membership::Join);
1066
1067        let mut cut = snap.clone();
1068        cut["auth_chain"].as_array_mut().unwrap().remove(0);
1069        let (r, b) = try_import(&cut);
1070        assert!(r.is_err(), "a chain with a link missing is refused");
1071        assert_eq!(count(&b, "SELECT COUNT(*) FROM rooms"), 0, "and nothing is left behind");
1072
1073        let mut altered = snap.clone();
1074        altered["auth_chain"][0]["origin_server_ts"] = Value::from(5);
1075        assert!(try_import(&altered).0.is_err(), "an altered link is refused");
1076    }
1077
1078    #[test]
1079    fn history_is_served_oldest_first_without_the_asked_for_events_and_skeletons_arrive_as_skeletons() {
1080        let mut a = conn();
1081        let room = room_with_bob_joined(&mut a);
1082        let before = extremity_ids(&a, &room).remove(0);
1083        let m1 = send(&mut a, &room, "t1", TS + 30);
1084        let m2 = send(&mut a, &room, "t2", TS + 31);
1085        let ids = |v: &[Value]| -> Vec<String> { v.iter().map(|p| wire_id(p).unwrap()).collect() };
1086
1087        // get_missing_events(earliest = before, latest = m2): exactly m1, nothing at or before `before`, not m2.
1088        let got = missing_events_json(&a, &room, "example.org", &[before.clone()], &[m2.event_id.clone()], 10);
1089        assert_eq!(ids(&got), vec![m1.event_id.clone()]);
1090        // backfill from m2: m2 first, newest to oldest.
1091        let bf = backfill_json(&a, &room, "example.org", &[m2.event_id.clone()], 100);
1092        let order = ids(&bf);
1093        assert_eq!(order[..2], [m2.event_id.clone(), m1.event_id.clone()]);
1094
1095        // m1's content is erased on A; what A serves from then on is the skeleton.
1096        assert!(skeletonize(&a, &m1.event_id).unwrap());
1097        let served = missing_events_json(&a, &room, "example.org", &[before.clone()], &[m2.event_id.clone()], 10);
1098        assert_eq!(served[0]["content"], serde_json::json!({}));
1099        assert_eq!(wire_id(&served[0]).unwrap(), m1.event_id, "same id, same signatures");
1100
1101        // A server that held the room at `before` takes the skeleton as history.
1102        let mut b = replica_conn();
1103        let snap = snapshot_json(&a, &room, &before).unwrap();
1104        let info = crate::fed_rooms::room_info(&a, &room).unwrap();
1105        let keys = a_keys(&a, &snapshot_events(&snap));
1106        import_snapshot(&mut b, &room, &info, &snap, &keys, NOW).unwrap();
1107        let keys = a_keys(&a, &served);
1108        assert_eq!(process_historic(&mut b, &room, &served, &keys, NOW).unwrap(), 1);
1109        let content: String = b.query_row("SELECT content FROM events WHERE event_id = ?1", [&m1.event_id], |r| r.get(0)).unwrap();
1110        assert_eq!(content, "{}");
1111        assert_eq!(count(&b, &format!("SELECT skeleton FROM dag_events WHERE event_id = '{}'", m1.event_id)), 1);
1112        // The same call again changes nothing, and a content-altered copy of a skeleton is refused.
1113        assert_eq!(process_historic(&mut b, &room, &served, &keys, NOW).unwrap(), 0);
1114        let mut forged = served.clone();
1115        forged[0]["content"] = serde_json::json!({"body": "x"});
1116        assert!(process_historic(&mut b, &room, &forged, &keys, NOW).is_err());
1117    }
1118
1119    fn peer_signer() -> Signer {
1120        let key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
1121        Signer::new("b.example", "ed25519:b1", move |m| key.sign(m).to_bytes())
1122    }
1123
1124    fn peer_keys(c: &Connection, pdu: &Value) -> PublicKeyMap {
1125        let mut keys = PublicKeyMap::new();
1126        let pk = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]).verifying_key().to_bytes();
1127        add_key(&mut keys, "b.example", "ed25519:b1", &key_to_b64(&pk)).unwrap();
1128        assert!(missing_keys(c, &[pdu], &mut keys).is_empty(), "our own keys resolve locally, the peer's was supplied");
1129        keys
1130    }
1131
1132    #[test]
1133    fn a_peers_join_is_checked_projected_and_a_stale_prev_forks_then_merges() {
1134        let mut c = conn();
1135        let room = create(&mut c, false);
1136        let alice = store::mxid_of(&c, 1).unwrap().unwrap();
1137        let bob = "@bob:b.example";
1138        let bob_uid = crate::fed_rooms::ensure_remote_user(&c, bob, NOW).unwrap();
1139        apply_invite(&mut c, &room, 1, &alice, InviteTarget { user_id: bob_uid, displayname: "bob" }, NOW, TS + 10).unwrap();
1140
1141        // Bob's server takes a template now (prev = the invite) ...
1142        let mut tpl = join_template(&c, &room, bob, TS + 20).unwrap();
1143        tpl["content"]["displayname"] = Value::String("bob".into());
1144        // ... but alice's server moves on before the join arrives.
1145        let m = send(&mut c, &room, "t1", TS + 25);
1146        let signed = dag::sign_template(&rules(), &peer_signer(), json_obj(&tpl).unwrap()).unwrap();
1147        let wire = serde_json::to_value(&signed.json).unwrap();
1148
1149        // A tampered copy and a copy signed by a stranger are refused and leave nothing behind.
1150        let rows = count(&c, "SELECT COUNT(*) FROM dag_events");
1151        let mut forged = wire.clone();
1152        forged["origin_server_ts"] = Value::from(1);
1153        assert!(recv(&mut c, "b.example", &forged).is_err());
1154        let stranger = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
1155        let other = Signer::new("b.example", "ed25519:b1", move |m| stranger.sign(m).to_bytes());
1156        let wrong = serde_json::to_value(&dag::sign_template(&rules(), &other, json_obj(&tpl).unwrap()).unwrap().json).unwrap();
1157        assert!(recv(&mut c, "b.example", &wrong).is_err());
1158        // Claiming another server's user is refused too.
1159        assert!(recv(&mut c, "c.example", &wire).is_err());
1160        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_events"), rows);
1161
1162        let got = recv(&mut c, "b.example", &wire).unwrap();
1163        assert_eq!(got.event_id, signed.event_id.as_str());
1164        assert!(got.forked && !got.soft_failed, "bob's join cites the invite, alice's message is a second branch");
1165        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_extremities"), 2);
1166        assert_eq!(store::room_member(&c, &room, bob_uid).unwrap().unwrap().membership, store::Membership::Join, "projected into the room's membership");
1167        assert!(recv(&mut c, "b.example", &wire).unwrap().duplicate);
1168
1169        // The next local event cites both branches and closes the fork.
1170        let m2 = send(&mut c, &room, "t2", TS + 50);
1171        assert_eq!(count(&c, "SELECT COUNT(*) FROM dag_extremities"), 1);
1172        let pdu: String = c.query_row("SELECT pdu FROM dag_events WHERE event_id = ?1", [&m2.event_id], |r| r.get(0)).unwrap();
1173        let v: Value = serde_json::from_str(&pdu).unwrap();
1174        assert_eq!(v["prev_events"].as_array().unwrap().len(), 2);
1175        assert!(v["prev_events"].as_array().unwrap().iter().any(|p| p == m.event_id.as_str()));
1176
1177        // The snapshot a peer would import carries the state and the extremity.
1178        let snap = snapshot_json(&c, &room, &m2.event_id).unwrap();
1179        assert!(snap["state"].as_array().unwrap().len() >= 7 && snap["extremities"].as_array().unwrap().len() == 1);
1180    }
1181}