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; 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 #[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 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}