Skip to main content

tuwunel_service/rooms/auth_chain/
mod.rs

1use std::{
2	collections::{BTreeSet, HashSet},
3	iter::once,
4	sync::{
5		Arc,
6		atomic::{AtomicBool, Ordering},
7	},
8	time::Instant,
9};
10
11use async_trait::async_trait;
12use futures::{
13	FutureExt, Stream, StreamExt, TryFutureExt, pin_mut,
14	stream::{FuturesUnordered, unfold},
15};
16use ruma::{
17	EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId,
18	room_version_rules::RoomVersionRules,
19};
20use serde::Deserialize;
21use tuwunel_core::{
22	Err, Result, at, debug, debug_error, err, implement,
23	itertools::Itertools,
24	matrix::room_version,
25	pdu::AuthEvents,
26	smallvec::SmallVec,
27	trace,
28	utils::{
29		IterStream,
30		stream::{BroadbandExt, ReadyExt, TryExpect, automatic_width},
31	},
32	validated, warn,
33};
34use tuwunel_database::Map;
35
36use crate::rooms::short::ShortEventId;
37
38pub struct Service {
39	services: Arc<crate::services::OnceServices>,
40	db: Data,
41}
42
43struct Data {
44	authchainkey_authchain: Arc<Map>,
45}
46
47type Bucket<'a> = BTreeSet<(ShortEventId, &'a EventId)>;
48type CacheKey = SmallVec<[ShortEventId; 1]>;
49
50#[async_trait]
51impl crate::Service for Service {
52	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
53		Ok(Arc::new(Self {
54			services: args.services.clone(),
55			db: Data {
56				authchainkey_authchain: args.db["authchainkey_authchain"].clone(),
57			},
58		}))
59	}
60
61	async fn clear_cache(&self) { self.db.authchainkey_authchain.clear().await; }
62
63	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
64}
65
66#[implement(Service)]
67pub fn event_ids_iter<'a, I>(
68	&'a self,
69	room_id: &'a RoomId,
70	room_version: &'a RoomVersionId,
71	starting_events: I,
72) -> impl Stream<Item = Result<OwnedEventId>> + Send + 'a
73where
74	I: Iterator<Item = &'a EventId> + Clone + ExactSizeIterator + Send + 'a,
75{
76	self.get_auth_chain(room_id, room_version, starting_events)
77		.map_ok(|chain| {
78			self.services
79				.short
80				.multi_get_eventid_from_short(chain.into_iter().stream())
81				.ready_filter(Result::is_ok)
82		})
83		.try_flatten_stream()
84}
85
86#[implement(Service)]
87#[tracing::instrument(
88	name = "auth_chain",
89	level = "debug",
90	skip_all,
91	fields(
92		%room_id,
93		starting_events = %starting_events.clone().count(),
94	)
95)]
96pub async fn get_auth_chain<'a, I>(
97	&'a self,
98	room_id: &RoomId,
99	room_version: &RoomVersionId,
100	starting_events: I,
101) -> Result<Vec<ShortEventId>>
102where
103	I: Iterator<Item = &'a EventId> + Clone + ExactSizeIterator + Send + 'a,
104{
105	const NUM_BUCKETS: usize = 50; //TODO: change possible w/o disrupting db?
106	const BUCKET: Bucket<'_> = BTreeSet::new();
107
108	let started = Instant::now();
109	let room_rules = room_version::rules(room_version)?;
110	let starting_events_count = starting_events.clone().count();
111	let starting_ids = self
112		.services
113		.short
114		.multi_get_or_create_shorteventid(starting_events.clone())
115		.zip(starting_events.stream());
116
117	pin_mut!(starting_ids);
118	let mut buckets = [BUCKET; NUM_BUCKETS];
119	while let Some((short, starting_event)) = starting_ids.next().await {
120		let bucket: usize = short.try_into()?;
121		let bucket: usize = validated!(bucket % NUM_BUCKETS);
122		buckets[bucket].insert((short, starting_event));
123	}
124
125	debug!(
126		starting_events = starting_events_count,
127		elapsed = ?started.elapsed(),
128		"start",
129	);
130
131	let full_auth_chain: Vec<ShortEventId> = buckets
132		.iter()
133		.stream()
134		.flat_map_unordered(automatic_width(), |starting_events| {
135			self.get_chunk_auth_chain(
136				room_id,
137				&started,
138				starting_events.iter().copied(),
139				&room_rules,
140			)
141			.boxed()
142		})
143		.collect::<Vec<_>>()
144		.map(IntoIterator::into_iter)
145		.map(Itertools::sorted_unstable)
146		.map(Itertools::dedup)
147		.map(Iterator::collect)
148		.boxed()
149		.await;
150
151	debug!(
152		chain_length = ?full_auth_chain.len(),
153		elapsed = ?started.elapsed(),
154		"done",
155	);
156
157	Ok(full_auth_chain)
158}
159
160#[implement(Service)]
161#[tracing::instrument(
162	name = "outer",
163	level = "trace",
164	skip_all,
165	fields(
166		starting_events = %starting_events.clone().count(),
167	)
168)]
169fn get_chunk_auth_chain<'a, I>(
170	&'a self,
171	room_id: &'a RoomId,
172	started: &'a Instant,
173	starting_events: I,
174	room_rules: &'a RoomVersionRules,
175) -> impl Stream<Item = ShortEventId> + Send + 'a
176where
177	I: Iterator<Item = (ShortEventId, &'a EventId)> + Clone + Send + Sync + 'a,
178{
179	self.get_cached_auth_chain(starting_events.clone().map(at!(0)))
180		.map_ok(IntoIterator::into_iter)
181		.map_ok(IterStream::try_stream)
182		.or_else(async move |_| {
183			let chain = self
184				.build_chunk_auth_chain(room_id, started, starting_events, room_rules)
185				.await;
186
187			Ok(chain.into_iter().try_stream())
188		})
189		.try_flatten_stream()
190		.map_expect("either cache hit or cache miss yields a chain")
191}
192
193#[implement(Service)]
194async fn build_chunk_auth_chain<'a, I>(
195	&'a self,
196	room_id: &'a RoomId,
197	started: &'a Instant,
198	starting_events: I,
199	room_rules: &'a RoomVersionRules,
200) -> Vec<ShortEventId>
201where
202	I: Iterator<Item = (ShortEventId, &'a EventId)> + Clone + Send + Sync + 'a,
203{
204	let chunk_complete = AtomicBool::new(true);
205
206	let build_chain = async |(shortid, event_id): (ShortEventId, &'a EventId)| {
207		if let Ok(cached) = self.get_cached_auth_chain(once(shortid)).await {
208			return cached;
209		}
210
211		let event_complete = AtomicBool::new(true);
212		let auth_chain: Vec<_> = self
213			.get_event_auth_chain(room_id, event_id, room_rules, &event_complete)
214			.collect()
215			.await;
216
217		match event_complete.load(Ordering::Relaxed) {
218			| true => self.put_cached_auth_chain(once(shortid), auth_chain.as_slice()),
219			| false => chunk_complete.store(false, Ordering::Relaxed),
220		}
221
222		debug!(
223			?event_id,
224			elapsed = ?started.elapsed(),
225			"Cache missed event"
226		);
227
228		auth_chain
229	};
230
231	let chunk_chain: Vec<_> = starting_events
232		.clone()
233		.stream()
234		.broad_then(build_chain)
235		.collect::<Vec<_>>()
236		.map(IntoIterator::into_iter)
237		.map(Iterator::flatten)
238		.map(Itertools::sorted_unstable)
239		.map(Itertools::dedup)
240		.map(Iterator::collect)
241		.await;
242
243	match chunk_complete.load(Ordering::Relaxed) {
244		| false => debug!(
245			elapsed = ?started.elapsed(),
246			"Incomplete chunk not cached",
247		),
248		| true => {
249			self.put_cached_auth_chain(starting_events.map(at!(0)), chunk_chain.as_slice());
250
251			debug!(
252				chunk_chain_length = ?chunk_chain.len(),
253				elapsed = ?started.elapsed(),
254				"Cache missed chunk",
255			);
256		},
257	}
258
259	chunk_chain
260}
261
262#[implement(Service)]
263#[tracing::instrument(name = "inner", level = "trace", skip_all)]
264fn get_event_auth_chain<'a>(
265	&'a self,
266	room_id: &'a RoomId,
267	event_id: &'a EventId,
268	room_rules: &'a RoomVersionRules,
269	complete: &'a AtomicBool,
270) -> impl Stream<Item = ShortEventId> + Send + 'a {
271	self.get_event_auth_chain_ids(room_id, event_id, room_rules, complete)
272		.broad_then(async move |auth_event| {
273			self.services
274				.short
275				.get_or_create_shorteventid(&auth_event)
276				.await
277		})
278}
279
280#[implement(Service)]
281#[tracing::instrument(
282	name = "inner_ids",
283	level = "trace",
284	skip_all,
285	fields(%event_id)
286)]
287fn get_event_auth_chain_ids<'a>(
288	&'a self,
289	room_id: &'a RoomId,
290	event_id: &'a EventId,
291	room_rules: &'a RoomVersionRules,
292	complete: &'a AtomicBool,
293) -> impl Stream<Item = OwnedEventId> + Send + 'a {
294	struct State<Fut> {
295		todo: FuturesUnordered<Fut>,
296		seen: HashSet<OwnedEventId>,
297	}
298
299	let starting_events = self.get_event_auth_event_ids(room_id, event_id.to_owned());
300
301	let state = State {
302		todo: once(starting_events).collect(),
303		seen: room_rules
304			.authorization
305			.room_create_event_id_as_room_id
306			.then_some(room_id.as_event_id().ok())
307			.into_iter()
308			.flatten()
309			.collect(),
310	};
311
312	let eval = |auth_events: AuthEvents, mut state: State<_>| {
313		let push = |auth_event: &OwnedEventId| {
314			trace!(todo = state.todo.len(), ?auth_event, "push");
315			state
316				.todo
317				.push(self.get_event_auth_event_ids(room_id, auth_event.clone()));
318		};
319
320		let seen = |auth_event: OwnedEventId| {
321			state
322				.seen
323				.insert(auth_event.clone())
324				.then_some(auth_event)
325		};
326
327		let out = auth_events
328			.into_iter()
329			.filter_map(seen)
330			.inspect(push)
331			.collect::<AuthEvents>()
332			.into_iter()
333			.stream();
334
335		(out, state)
336	};
337
338	// unfold requires FnMut returning a nameable future; an async closure
339	// capturing eval does not satisfy it.
340	#[expect(closure_returning_async_block)]
341	unfold(state, move |mut state| async move {
342		match state.todo.next().await {
343			| None => None,
344			| Some(Ok(auth_events)) => Some(eval(auth_events, state)),
345			| Some(Err(e)) => {
346				complete.store(false, Ordering::Relaxed);
347
348				// A missing ancestor is a normal backfill gap and is skipped;
349				// any other error is corrupt data and ends the walk.
350				e.is_not_found()
351					.then(move || (AuthEvents::new().into_iter().stream(), state))
352			},
353		}
354	})
355	.flatten()
356}
357
358#[implement(Service)]
359#[tracing::instrument(
360	name = "cache_put",
361	level = "debug",
362	skip_all,
363	fields(
364		key_len = key.clone().count(),
365		chain_len = auth_chain.len(),
366	)
367)]
368fn put_cached_auth_chain<I>(&self, key: I, auth_chain: &[ShortEventId])
369where
370	I: Iterator<Item = ShortEventId> + Clone + Send,
371{
372	let key = key.collect::<CacheKey>();
373
374	debug_assert!(!key.is_empty(), "auth_chain key must not be empty");
375
376	self.db
377		.authchainkey_authchain
378		.put(key.as_slice(), auth_chain);
379}
380
381#[implement(Service)]
382#[tracing::instrument(
383	name = "cache_get",
384	level = "trace",
385	err(level = "trace"),
386	skip_all,
387	fields(
388		key_len = %key.clone().count()
389	),
390)]
391async fn get_cached_auth_chain<I>(&self, key: I) -> Result<Vec<ShortEventId>>
392where
393	I: Iterator<Item = ShortEventId> + Clone + Send,
394{
395	let key = key.collect::<CacheKey>();
396
397	if key.is_empty() {
398		return Ok(Vec::new());
399	}
400
401	let chain = self
402		.db
403		.authchainkey_authchain
404		.qry(key.as_slice())
405		.map_err(|_| err!(Request(NotFound("auth_chain not cached"))))
406		.await?
407		.as_chunks::<{ size_of::<u64>() }>()
408		.0
409		.iter()
410		.copied()
411		.map(u64::from_be_bytes)
412		.collect();
413
414	Ok(chain)
415}
416
417#[implement(Service)]
418#[tracing::instrument(
419	name = "auth_events",
420	level = "trace",
421	ret(level = "trace"),
422	err(level = "trace"),
423	skip_all,
424	fields(%event_id)
425)]
426async fn get_event_auth_event_ids<'a>(
427	&'a self,
428	room_id: &'a RoomId,
429	event_id: OwnedEventId,
430) -> Result<AuthEvents> {
431	#[derive(Deserialize)]
432	struct Pdu {
433		auth_events: AuthEvents,
434		room_id: OwnedRoomId,
435	}
436
437	let pdu: Pdu = self
438		.services
439		.timeline
440		.get(&event_id)
441		.inspect_err(|e| {
442			debug_error!(?event_id, ?room_id, "auth chain event: {e}");
443		})
444		.await?;
445
446	if pdu.room_id != room_id {
447		return Err!(Request(Forbidden(error!(
448			?event_id,
449			?room_id,
450			wrong_room_id = ?pdu.room_id,
451			"auth event for incorrect room",
452		))));
453	}
454
455	Ok(pdu.auth_events)
456}