Skip to main content

tuwunel_matrix/room/state/resolution/
resolve.rs

1#[cfg(test)]
2mod tests;
3
4mod auth_difference;
5mod conflicted_subgraph;
6mod iterative_auth_check;
7mod mainline_sort;
8mod power_sort;
9mod split_conflicted;
10
11use std::{
12	collections::{BTreeMap, HashSet},
13	ops::Deref,
14	vec::IntoIter,
15};
16
17use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
18use ruma::{OwnedEventId, events::StateEventType, room_version_rules::RoomVersionRules};
19use tuwunel_core::{
20	Result, debug,
21	itertools::Itertools,
22	smallvec::SmallVec,
23	trace,
24	utils::{
25		BoolExt,
26		stream::{BroadbandExt, IterStream, ReadyExt},
27	},
28};
29
30use self::{
31	auth_difference::auth_difference, conflicted_subgraph::conflicted_subgraph_dfs,
32	iterative_auth_check::iterative_auth_check, mainline_sort::mainline_sort,
33	power_sort::power_sort, split_conflicted::split_conflicted_state,
34};
35use super::FetchEvent;
36#[cfg(test)]
37use super::test_utils;
38use crate::{TypeStateKey, event_id::RandomState};
39
40/// A mapping of event type and state_key to some value `T`, usually an
41/// `EventId`.
42pub type StateMap<Id> = BTreeMap<TypeStateKey, Id>;
43
44/// Full recursive auth chain for one candidate [`StateMap`].
45///
46/// Values are immutable after construction and normally distinct. Their order
47/// is arbitrary, and consumers must not depend on it.
48#[derive(Clone)]
49pub struct AuthSet<Id>(Vec<Id>);
50
51/// Conflicting event ids for each contested state key.
52pub type ConflictMap<Id> = StateMap<ConflictVec<Id>>;
53
54/// Event ids contesting one state key.
55///
56/// Two forks disputing a key is the modal conflict, so two ids stay inline.
57type ConflictVec<Id> = SmallVec<[Id; 2]>;
58
59/// The full conflicted set (arbitrary order).
60type ConflictedSet = HashSet<OwnedEventId, RandomState>;
61
62impl<Id> AuthSet<Id> {
63	/// Creates an auth set from identifiers expected to be distinct.
64	///
65	/// Duplicates are neither checked nor removed, so hot paths avoid
66	/// redundant work. A damaged short-id mapping can still repeat an
67	/// identifier, which the auth difference counts once per set.
68	#[inline]
69	#[must_use]
70	pub fn from_distinct(ids: Vec<Id>) -> Self { Self(ids) }
71}
72
73impl<Id> Default for AuthSet<Id> {
74	fn default() -> Self { Self(Vec::new()) }
75}
76
77impl<Id: Ord> FromIterator<Id> for AuthSet<Id> {
78	fn from_iter<I: IntoIterator<Item = Id>>(iter: I) -> Self {
79		Self::from_distinct(
80			iter.into_iter()
81				.sorted_unstable()
82				.dedup()
83				.collect(),
84		)
85	}
86}
87
88impl<Id> IntoIterator for AuthSet<Id> {
89	type IntoIter = IntoIter<Id>;
90	type Item = Id;
91
92	fn into_iter(self) -> Self::IntoIter { self.0.into_iter() }
93}
94
95/// Resolves the state of a room with the version 2 [state resolution] algorithm.
96///
97/// Each of `state_maps` is one possible fork of the room's state, and
98/// `auth_sets` holds the full recursive auth chain of each fork's events, where
99/// an event repeated within one set counts once. `fetch` loads an event by ID,
100/// and `rules` are those of the room's version. The caller must ensure that all
101/// the events are from the same room.
102///
103/// [state resolution]: https://spec.matrix.org/latest/rooms/v2/#state-resolution
104#[tracing::instrument(level = "debug", skip_all)]
105pub async fn resolve<States, AuthSets, Fetch>(
106	rules: &RoomVersionRules,
107	state_maps: States,
108	auth_sets: AuthSets,
109	fetch: Fetch,
110	hydra_backports: bool,
111) -> Result<StateMap<OwnedEventId>>
112where
113	States: Stream<Item = StateMap<OwnedEventId>> + Send,
114	AuthSets: Stream<Item = AuthSet<OwnedEventId>> + Send,
115	Fetch: FetchEvent,
116{
117	// Split the unconflicted state map and the conflicted state set.
118	let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await;
119
120	debug!(
121		unconflicted = unconflicted_state.len(),
122		conflicted_states = conflicted_states.len(),
123		conflicted_events = conflicted_states
124			.values()
125			.fold(0_usize, |a, s| a.saturating_add(s.len())),
126		"unresolved states"
127	);
128
129	trace!(
130		?unconflicted_state,
131		?conflicted_states,
132		unconflicted = unconflicted_state.len(),
133		conflicted_states = conflicted_states.len(),
134		"unresolved states"
135	);
136
137	if conflicted_states.is_empty() {
138		return Ok(unconflicted_state.into_iter().collect());
139	}
140
141	// 0. The full conflicted set is the union of the conflicted state set and the
142	//    auth difference. Don't honor events that don't exist.
143	let full_conflicted_set =
144		full_conflicted_set(rules, conflicted_states, auth_sets, fetch, hydra_backports).await?;
145
146	// 1. Select the set X of all power events that appear in the full conflicted
147	//    set. For each such power event P, enlarge X by adding the events in the
148	//    auth chain of P which also belong to the full conflicted set. Sort X into
149	//    a list using the reverse topological power ordering.
150	let sorted_power_set: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
151		.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
152		.inspect_ok(|list| trace!(?list, "sorted power events"))
153		.boxed() // size firewall
154		.await?;
155
156	let power_set_event_ids: Vec<_> = sorted_power_set
157		.iter()
158		.sorted_unstable()
159		.collect();
160
161	let sorted_power_set = sorted_power_set
162		.iter()
163		.stream()
164		.map(AsRef::as_ref);
165
166	let begin_with_empty_state_map = rules
167		.state_res
168		.v2_rules()
169		.is_some_and(|r| r.begin_iterative_auth_checks_with_empty_state_map)
170		|| hydra_backports;
171
172	let initial_state = begin_with_empty_state_map
173		.is_false()
174		.then(|| unconflicted_state.clone())
175		.unwrap_or_default();
176
177	// 2. Apply the iterative auth checks algorithm, starting from the unconflicted
178	//    state map, to the list of events from the previous step to get a partially
179	//    resolved state.
180	// query-depth firewall
181	let partially_resolved_state =
182		iterative_auth_check(rules, sorted_power_set, initial_state, fetch)
183			.boxed()
184			.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
185			.inspect_ok(|map| trace!(?map, "partially resolved power state"))
186			.await?;
187
188	// This "epochs" power level event
189	let power_ty_sk = (StateEventType::RoomPowerLevels, "".into());
190	let power_event = partially_resolved_state.get(&power_ty_sk);
191	debug!(event_id = ?power_event, "epoch power event");
192
193	let remaining_events: Vec<_> = full_conflicted_set
194		.into_iter()
195		.filter(|id| power_set_event_ids.binary_search(&id).is_err())
196		.collect();
197
198	debug!(count = remaining_events.len(), "remaining events");
199	trace!(list = ?remaining_events, "remaining events");
200
201	let have_remaining_events = !remaining_events.is_empty();
202	let remaining_events = remaining_events
203		.iter()
204		.stream()
205		.map(AsRef::as_ref);
206
207	// 3. Take all remaining events that weren’t picked in step 1 and order them by
208	//    the mainline ordering based on the power level in the partially resolved
209	//    state obtained in step 2.
210	let sorted_remaining_events = have_remaining_events
211		.then_async(move || mainline_sort(power_event.cloned(), remaining_events, fetch))
212		.boxed(); // size firewall
213
214	let sorted_remaining_events = sorted_remaining_events
215		.await
216		.unwrap_or(Ok(Vec::new()))?;
217
218	debug!(count = sorted_remaining_events.len(), "sorted remaining events");
219	trace!(list = ?sorted_remaining_events, "sorted remaining events");
220
221	let sorted_remaining_events = sorted_remaining_events
222		.iter()
223		.stream()
224		.map(AsRef::as_ref);
225
226	// 4. Apply the iterative auth checks algorithm on the partial resolved state
227	//    and the list of events from the previous step.
228	// query-depth firewall
229	let mut resolved_state =
230		iterative_auth_check(rules, sorted_remaining_events, partially_resolved_state, fetch)
231			.boxed()
232			.await?;
233
234	// 5. Update the result by replacing any event with the event with the same key
235	//    from the unconflicted state map, if such an event exists, to get the final
236	//    resolved state.
237	resolved_state.extend(unconflicted_state);
238
239	debug!(resolved_state = resolved_state.len(), "resolved state");
240	trace!(?resolved_state, "resolved state");
241
242	Ok(resolved_state)
243}
244
245#[tracing::instrument(
246	name = "conflicted",
247	level = "debug",
248	skip_all,
249	fields(
250		states = conflicted_states.len(),
251		events = conflicted_states.values().flatten().count()
252	),
253)]
254async fn full_conflicted_set<AuthSets>(
255	rules: &RoomVersionRules,
256	conflicted_states: ConflictMap<OwnedEventId>,
257	auth_sets: AuthSets,
258	fetch: impl FetchEvent,
259	hydra_backports: bool,
260) -> Result<ConflictedSet>
261where
262	AuthSets: Stream<Item = AuthSet<OwnedEventId>> + Send,
263{
264	let consider_conflicted_subgraph = rules
265		.state_res
266		.v2_rules()
267		.is_some_and(|rules| rules.consider_conflicted_state_subgraph)
268		|| hydra_backports;
269
270	let conflicted_state_set: Vec<_> = conflicted_states
271		.values()
272		.flatten()
273		.sorted_unstable()
274		.dedup()
275		.collect();
276
277	// Since `org.matrix.hydra.11`, fetch the conflicted state subgraph.
278	let conflicted_subgraph = consider_conflicted_subgraph
279		.then_async(async || conflicted_subgraph_dfs(&conflicted_state_set, fetch))
280		.map(Option::into_iter)
281		.map(IterStream::stream)
282		.flatten_stream()
283		.flatten()
284		.boxed(); // erase region
285
286	let conflicted_state_ids = conflicted_state_set
287		.iter()
288		.map(Deref::deref)
289		.cloned()
290		.stream();
291
292	auth_difference(auth_sets)
293		.chain(conflicted_state_ids)
294		.broad_then(async |id| {
295			let exists = fetch.exists(&id).await?;
296
297			Ok(exists.then_some(id))
298		})
299		.ready_filter_map(Result::transpose)
300		.chain(conflicted_subgraph)
301		.try_collect::<ConflictedSet>()
302		.inspect_ok(|set| debug!(count = set.len(), "full conflicted set"))
303		.inspect_ok(|set| trace!(?set, "full conflicted set"))
304		.await
305}