From d71d94a0c8048977a189298c59d3815153082a1e Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Sat, 12 Jul 2025 21:43:38 +0100 Subject: [PATCH] feat: MSC4297, State Resolution v2.1 --- Cargo.lock | 22 +++---- src/service/rooms/auth_chain/mod.rs | 85 +++++++++++++++++++++++++- src/service/rooms/event_handler/mod.rs | 45 +++++++++++--- 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bde25c40..b205f7f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2521,7 +2521,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.6" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "assign", "js_int", @@ -2540,7 +2540,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.12.2" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "js_int", "ruma-common", @@ -2552,7 +2552,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "as_variant", "assign", @@ -2575,7 +2575,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "as_variant", "base64 0.22.1", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.5" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "as_variant", "indexmap 2.9.0", @@ -2631,7 +2631,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "bytes", "headers", @@ -2653,7 +2653,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "js_int", "thiserror 2.0.12", @@ -2662,7 +2662,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "cfg-if", "proc-macro-crate", @@ -2677,7 +2677,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.11.0" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "js_int", "ruma-common", @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.17.1" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -2705,7 +2705,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.13.0" -source = "git+https://github.com/ruma/ruma.git#796cc9f3aacb7e69f6ec3a0d5c2ba900c3e65910" +source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" dependencies = [ "js_int", "ruma-common", diff --git a/src/service/rooms/auth_chain/mod.rs b/src/service/rooms/auth_chain/mod.rs index a4838f7f..10be8586 100644 --- a/src/service/rooms/auth_chain/mod.rs +++ b/src/service/rooms/auth_chain/mod.rs @@ -5,7 +5,7 @@ use std::{ }; pub use data::Data; -use ruma::{api::client::error::ErrorKind, EventId, RoomId}; +use ruma::{api::client::error::ErrorKind, state_res::StateMap, EventId, RoomId}; use tracing::{debug, error, warn}; use crate::{services, Error, Result}; @@ -161,4 +161,87 @@ impl Service { Ok(found) } + + #[tracing::instrument(skip(self, conflicted_state_set))] + /// Fetches the conflicted state subgraph of the given events + pub fn get_conflicted_state_subgraph( + &self, + room_id: &RoomId, + conflicted_state_set: &StateMap>>, + ) -> Result>> { + let conflicted_event_ids: HashSet<_> = + conflicted_state_set.values().flatten().cloned().collect(); + let mut conflicted_state_subgraph = HashSet::new(); + + let mut stack = vec![conflicted_event_ids.iter().cloned().collect::>()]; + let mut path = Vec::new(); + + let mut seen_events = HashSet::new(); + + let next_event = |stack: &mut Vec>, path: &mut Vec<_>| { + while stack.last().is_some_and(|s| s.is_empty()) { + stack.pop(); + path.pop(); + } + + stack.last_mut().and_then(|s| s.pop()) + }; + + while let Some(event_id) = next_event(&mut stack, &mut path) { + path.push(event_id.clone()); + + if conflicted_state_subgraph.contains(&event_id) { + // If we reach a conflicted state subgraph path, this path must also be part of + // the conflicted state subgraph, as we will eventually reach a conflicted event + // if we follow this path. + // + // We check if path > 1 here and below, as we don't consider a single conflicted + // event to be a path from one conflicted to another. + if path.len() > 1 { + conflicted_state_subgraph.extend(path.iter().cloned()); + } + + // All possible paths from this event must have been traversed in the iteration + // that caused this event to be added to the conflicted state subgraph in the first + // place. + // + // We pop the path here and below as it won't be removed by `next_event`, due to us + // never pushing it's auth events to the stack. + path.pop(); + continue; + } + + if conflicted_event_ids.contains(&event_id) && path.len() > 1 { + conflicted_state_subgraph.extend(path.iter().cloned()); + } + + if seen_events.contains(&event_id) { + // All possible paths from this event must have been traversed in the iteration + // that caused this event to be added to the conflicted state subgraph in the first + // place. + path.pop(); + continue; + } + + if let Some(pdu) = services().rooms.timeline.get_pdu(&event_id)? { + if pdu.room_id().as_ref() != room_id { + return Err(Error::BadRequest( + ErrorKind::forbidden(), + "Evil event in db", + )); + } + + stack.push(pdu.auth_events.clone()); + } else { + warn!(?event_id, "Could not find pdu mentioned in auth events"); + return Err(Error::BadDatabase( + "Missing auth event for PDU stored in database", + )); + } + + seen_events.insert(event_id); + } + + Ok(conflicted_state_subgraph) + } } diff --git a/src/service/rooms/event_handler/mod.rs b/src/service/rooms/event_handler/mod.rs index 3b56903e..47a479d6 100644 --- a/src/service/rooms/event_handler/mod.rs +++ b/src/service/rooms/event_handler/mod.rs @@ -31,7 +31,7 @@ use ruma::{ StateEventType, TimelineEventType, }, int, - room_version_rules::{AuthorizationRules, RoomVersionRules}, + room_version_rules::{AuthorizationRules, RoomVersionRules, StateResolutionV2Rules}, state_res::{self, StateMap}, uint, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedServerName, OwnedServerSigningKeyId, RoomId, ServerName, @@ -709,10 +709,11 @@ impl Service { let lock = services().globals.stateres_mutex.lock(); let result = state_res::resolve( - &room_version_id - .rules() - .expect("Supported room version has rules") - .authorization, + &room_version_rules.authorization, + room_version_rules + .state_res + .v2_rules() + .expect("We only support room versions using state resolution v2"), &fork_states, auth_chain_sets, |id| { @@ -722,6 +723,13 @@ impl Service { } res.ok().flatten() }, + |css| { + services() + .rooms + .auth_chain + .get_conflicted_state_subgraph(room_id, css) + .ok() + }, ); drop(lock); @@ -961,7 +969,15 @@ impl Service { } let new_room_state = self - .resolve_state(room_id, &room_version_rules.authorization, state_after) + .resolve_state( + room_id, + &room_version_rules.authorization, + room_version_rules + .state_res + .v2_rules() + .expect("We only support room versions using state resolution v2"), + state_after, + ) .await?; // Set the new room state to the resolved state @@ -1039,6 +1055,7 @@ impl Service { &self, room_id: &RoomId, auth_rules: &AuthorizationRules, + state_res_rules: &StateResolutionV2Rules, incoming_state: HashMap>, ) -> Result>> { debug!("Loading current room state ids"); @@ -1097,8 +1114,20 @@ impl Service { }; let lock = services().globals.stateres_mutex.lock(); - let state = match state_res::resolve(auth_rules, &fork_states, auth_chain_sets, fetch_event) - { + let state = match state_res::resolve( + auth_rules, + state_res_rules, + &fork_states, + auth_chain_sets, + fetch_event, + |css| { + services() + .rooms + .auth_chain + .get_conflicted_state_subgraph(room_id, css) + .ok() + }, + ) { Ok(new_state) => new_state, Err(_) => { return Err(Error::bad_database("State resolution failed, either an event could not be found or deserialization"));