1use std::{
2 cmp::Ordering,
3 collections::{BTreeMap, BTreeSet},
4 sync::Arc,
5};
6
7use futures::StreamExt;
8use serde::Deserialize;
9use tuwunel_core::{
10 Result, err, implement, info,
11 smallvec::SmallVec,
12 utils::{
13 BoolExt, ReadyExt,
14 stream::{IterStream, TryIgnore},
15 },
16 warn,
17};
18use tuwunel_database::{Database, Get, Handle, Map, SEP};
19
20use crate::{Services, rooms::pdu_metadata::typed_relations::Key as RelationKey};
21
22type Identity = SmallVec<[u8; 48]>;
26
27pub(super) type Relations = Vec<(RelationKey, u64)>;
32
33type Bits = Vec<u64>;
38
39type Candidate = (u64, Identity);
41
42type Candidates = Vec<Candidate>;
45
46type Resolution = (Vec<u64>, BTreeMap<u64, u64>, Candidates, u64);
52
53enum Resolved {
59 Winner(u64),
60 Absent,
61 Unsettled,
62}
63
64const MAX_SHORT: u64 = 1 << 30;
70
71#[derive(Default)]
79pub(super) struct Family {
80 pub(super) rows: u64,
81 pub(super) losers: Vec<u64>,
82 pub(super) winners: BTreeMap<u64, u64>,
83 pub(super) dangling: Candidates,
84 pub(super) promotable: Candidates,
85 pub(super) contended: u64,
86 pub(super) unresolved: u64,
87 pub(super) malformed: u64,
88}
89
90#[derive(Default)]
95pub(super) struct Scan {
96 pub(super) events: Family,
97 pub(super) statekeys: Family,
98 pub(super) dirty: u64,
99 pub(super) entries: u64,
100 pub(super) infected: BTreeSet<u64>,
101 pub(super) orphans: u64,
102 pub(super) missing_parents: u64,
103 pub(super) infected_parents: u64,
104 pub(super) malformed_diffs: u64,
105 pub(super) moves: Vec<u64>,
106 pub(super) relations: Relations,
107 pub(super) strays: u64,
108 pub(super) unverifiable: bool,
109}
110
111struct Diffs<'a> {
117 counter: u64,
118 event_stale: &'a [u64],
119 statekey_stale: &'a [u64],
120 event_reverse: &'a [u64],
121 statekey_reverse: &'a [u64],
122}
123
124#[derive(Default)]
130struct Counts {
131 infected: BTreeSet<u64>,
132 ghosts: u64,
133 removed_ghosts: u64,
134 orphans: u64,
135 missing_parents: u64,
136 malformed: u64,
137}
138
139#[derive(Deserialize)]
144struct Notification {
145 sroomid: u64,
146}
147
148#[tracing::instrument(level = "debug", skip_all)]
154pub(super) async fn scan(services: &Services) -> Result<Scan> {
155 info!("Scanning ShortID columns for duplicate values...");
156
157 let counter = services.globals.current_count();
158
159 if counter >= MAX_SHORT {
160 warn!(
161 %counter,
162 "Short id space too large to verify injectivity; stale auth chain caches, if \
163 any, survive and can distort state resolution until `server clear-caches`."
164 );
165 return Ok(Scan { unverifiable: true, ..Default::default() });
166 }
167
168 let words = usize::try_from((counter / 64).saturating_add(1))
169 .map_err(|_| err!("short id bitmap exceeds the address width"))?;
170
171 let (events, event_reverse) =
172 family(services, "eventid_shorteventid", "shorteventid_eventid", counter, words).await;
173
174 let (statekeys, statekey_reverse) =
175 family(services, "statekey_shortstatekey", "shortstatekey_statekey", counter, words)
176 .await;
177
178 if events.losers.is_empty() && statekeys.losers.is_empty() {
179 return Ok(Scan { events, statekeys, ..Default::default() });
180 }
181
182 let swept =
183 sweep(services, &events, &statekeys, event_reverse, statekey_reverse, counter).await;
184
185 let scan = Scan { events, statekeys, ..swept };
186
187 match scan.anomalous() || scan.healable() {
190 | true => Ok(scan),
191 | false => {
192 let strays = strays(&services.db, counter, words).await;
193
194 Ok(Scan { strays, ..scan })
195 },
196 }
197}
198
199#[implement(Scan)]
206pub(super) fn anomalous(&self) -> bool {
207 self.events.anomalous()
208 || self.statekeys.anomalous()
209 || self.orphans > 0
210 || self.missing_parents > 0
211 || self.infected_parents > 0
212 || self.malformed_diffs > 0
213}
214
215#[implement(Scan)]
220pub(super) fn healable(&self) -> bool { self.events.healable() || self.statekeys.healable() }
221
222#[implement(Family)]
228fn anomalous(&self) -> bool { self.contended > 0 || self.unresolved > 0 || self.malformed > 0 }
229
230#[implement(Family)]
236pub(super) fn healable(&self) -> bool {
237 !self.anomalous() && (!self.dangling.is_empty() || !self.promotable.is_empty())
238}
239
240#[tracing::instrument(
250 level = "debug",
251 skip_all,
252 fields(
253 %forward,
254 %reverse,
255 ),
256)]
257async fn family(
258 services: &Services,
259 forward: &'static str,
260 reverse: &'static str,
261 counter: u64,
262 words: usize,
263) -> (Family, Bits) {
264 let db = &services.db;
265
266 let (reverse_bits, rows, reverse_malformed) = reverse_bitmap(&db[reverse], words).await;
267
268 let (forward_bits, mut dangling, forward_malformed) =
269 dangling_winners(&db[forward], &reverse_bits, counter, words).await;
270
271 let candidates = match any_unclaimed(&reverse_bits, &forward_bits, counter) {
273 | false => Candidates::new(),
274 | true => loser_candidates(&db[reverse], &forward_bits, counter).await,
275 };
276
277 drop(forward_bits);
278
279 let (losers, winners, mut promotable, unresolved) = resolve(&db[forward], &candidates).await;
280
281 let contended = contenders(&mut dangling, by_short)
282 .saturating_add(contenders(&mut promotable, by_identity));
283
284 let family = Family {
285 rows,
286 losers,
287 winners,
288 dangling,
289 promotable,
290 contended,
291 unresolved,
292 malformed: reverse_malformed.saturating_add(forward_malformed),
293 };
294
295 info!(
296 %forward,
297 %reverse,
298 rows = family.rows,
299 losers = family.losers.len(),
300 dangling = family.dangling.len(),
301 promotable = family.promotable.len(),
302 contended = family.contended,
303 unresolved = family.unresolved,
304 malformed = family.malformed,
305 "Finished scanning column pair."
306 );
307
308 (family, reverse_bits)
309}
310
311async fn reverse_bitmap(map: &Arc<Map>, words: usize) -> (Bits, u64, u64) {
317 map.raw_keys()
318 .ignore_err()
319 .ready_fold((vec![0_u64; words], 0_u64, 0_u64), |(mut bits, rows, malformed), key| {
320 let rows = rows.saturating_add(1);
321
322 match short_of(key) {
323 | None => (bits, rows, malformed.saturating_add(1)),
324 | Some(short) => {
325 set_bit(&mut bits, short);
326
327 (bits, rows, malformed)
328 },
329 }
330 })
331 .await
332}
333
334async fn dangling_winners(
341 map: &Arc<Map>,
342 reverse_bits: &[u64],
343 counter: u64,
344 words: usize,
345) -> (Bits, Candidates, u64) {
346 map.raw_stream()
347 .ignore_err()
348 .ready_fold(
349 (vec![0_u64; words], Candidates::new(), 0_u64),
350 |(mut bits, mut dangling, malformed), (key, value)| match short_of(value) {
351 | None => (bits, dangling, malformed.saturating_add(1)),
352 | Some(short) => {
353 if short <= counter && !get_bit(reverse_bits, short) {
354 dangling.push((short, Identity::from_slice(key)));
355 }
356
357 set_bit(&mut bits, short);
358
359 (bits, dangling, malformed)
360 },
361 },
362 )
363 .await
364}
365
366fn any_unclaimed(reverse_bits: &[u64], forward_bits: &[u64], counter: u64) -> bool {
372 let last = usize::try_from(counter / 64).unwrap_or(usize::MAX);
373 let tail = u64::MAX >> 63_u64.saturating_sub(counter % 64);
374
375 debug_assert_eq!(reverse_bits.len(), last.saturating_add(1), "bitmap spans the counter");
376 debug_assert_eq!(forward_bits.len(), reverse_bits.len(), "bitmaps span one id space");
377
378 reverse_bits
379 .iter()
380 .copied()
381 .zip(forward_bits.iter().copied())
382 .enumerate()
383 .any(|(word, (reverse, forward))| {
384 let mask = match word < last {
385 | true => u64::MAX,
386 | false => tail,
387 };
388
389 (reverse & !forward & mask) != 0
390 })
391}
392
393async fn loser_candidates(map: &Arc<Map>, forward_bits: &[u64], counter: u64) -> Candidates {
397 map.raw_stream()
398 .ignore_err()
399 .ready_fold(Candidates::new(), |mut candidates, (key, value)| {
400 let unclaimed =
401 short_of(key).filter(|short| *short <= counter && !get_bit(forward_bits, *short));
402
403 if let Some(short) = unclaimed {
404 candidates.push((short, Identity::from_slice(value)));
405 }
406
407 candidates
408 })
409 .await
410}
411
412async fn resolve(map: &Arc<Map>, candidates: &[(u64, Identity)]) -> Resolution {
418 let (mut losers, winners, promotable, unsettled, paired) = candidates
419 .iter()
420 .map(candidate_identity)
421 .stream()
422 .get(map)
423 .map(resolution)
424 .zip(candidates.iter().stream())
425 .ready_fold(
426 (Vec::new(), BTreeMap::new(), Candidates::new(), 0_u64, 0_usize),
427 |(mut losers, mut winners, mut promotable, unsettled, paired),
428 (resolved, candidate)| {
429 let paired = paired.saturating_add(1);
430 let loser = candidate_short(candidate);
431
432 match resolved {
433 | Resolved::Winner(winner) if winner == loser =>
434 (losers, winners, promotable, unsettled, paired),
435 | Resolved::Winner(winner) => {
436 losers.push(loser);
437 winners.insert(loser, winner);
438
439 (losers, winners, promotable, unsettled, paired)
440 },
441 | Resolved::Absent => {
442 losers.push(loser);
443 promotable.push(candidate.clone());
444
445 (losers, winners, promotable, unsettled, paired)
446 },
447 | Resolved::Unsettled => {
448 losers.push(loser);
449
450 (losers, winners, promotable, unsettled.saturating_add(1), paired)
451 },
452 }
453 },
454 )
455 .await;
456
457 let tail = candidates.get(paired..).unwrap_or_default();
460 losers.extend(tail.iter().map(candidate_short));
461
462 let unresolved = unsettled.saturating_add(u64::try_from(tail.len()).unwrap_or(u64::MAX));
463
464 (losers, winners, promotable, unresolved)
465}
466
467fn contenders<F>(candidates: &mut [Candidate], cmp: F) -> u64
474where
475 F: Fn(&Candidate, &Candidate) -> Ordering,
476{
477 candidates.sort_unstable_by(&cmp);
478
479 let contenders = candidates
480 .windows(2)
481 .filter(|pair| cmp(&pair[0], &pair[1]).is_eq())
482 .count();
483
484 u64::try_from(contenders).unwrap_or(u64::MAX)
485}
486
487fn candidate_identity((_, identity): &Candidate) -> &Identity { identity }
490
491fn candidate_short((short, _): &Candidate) -> u64 { *short }
492
493fn by_short(a: &Candidate, b: &Candidate) -> Ordering { a.0.cmp(&b.0) }
494
495fn by_identity(a: &Candidate, b: &Candidate) -> Ordering { a.1.cmp(&b.1) }
496
497fn resolution(result: Result<Handle<'_>>) -> Resolved {
500 match result {
501 | Ok(handle) => short_of(&handle).map_or(Resolved::Unsettled, Resolved::Winner),
502 | Err(error) if error.is_not_found() => Resolved::Absent,
503 | Err(_) => Resolved::Unsettled,
504 }
505}
506
507#[tracing::instrument(level = "debug", skip_all)]
513async fn sweep(
514 services: &Services,
515 events: &Family,
516 statekeys: &Family,
517 event_reverse: Bits,
518 statekey_reverse: Bits,
519 counter: u64,
520) -> Scan {
521 let db = &services.db;
522 let words = event_reverse.len();
523 let event_stale = bits_of(&events.losers, words);
524 let statekey_stale = bits_of(&statekeys.losers, words);
525
526 let walk = Diffs {
527 counter,
528 event_stale: &event_stale,
529 statekey_stale: &statekey_stale,
530 event_reverse: &event_reverse,
531 statekey_reverse: &statekey_reverse,
532 };
533
534 let counts = diffs(db, words, walk).await;
535
536 drop(event_reverse);
537 drop(statekey_reverse);
538
539 let infected_parents = match counts.infected.is_empty() {
543 | true => 0,
544 | false =>
545 db["shortstatehash_statediff"]
546 .raw_stream()
547 .ignore_err()
548 .ready_fold(0_u64, |descendants, (_, value)| {
549 let parent = value.get(0..8).and_then(short_of);
550
551 descendants.saturating_add(u64::from(
552 parent.is_some_and(|parent| counts.infected.contains(&parent)),
553 ))
554 })
555 .await,
556 };
557
558 let (dirty, entries) = db["authchainkey_authchain"]
561 .raw_stream()
562 .ignore_err()
563 .ready_fold((0_u64, 0_u64), |(dirty, entries), (key, chain)| {
564 let hit = disposable(key, &event_stale, &statekey_stale)
565 || disposable(chain, &event_stale, &statekey_stale);
566
567 (dirty.saturating_add(u64::from(hit)), entries.saturating_add(1))
568 })
569 .await;
570
571 let moves: Vec<u64> = db["shorteventid_shortstatehash"]
574 .raw_keys()
575 .ignore_err()
576 .ready_fold(Vec::new(), |mut moves, key| {
577 if let Some(loser) = short_of(key).filter(|short| get_bit(&event_stale, *short)) {
578 moves.push(loser);
579 }
580
581 moves
582 })
583 .await;
584
585 let relations: Relations = db["relatesto_typed"]
586 .raw_stream()
587 .ignore_err()
588 .ready_fold(Relations::new(), |mut relations, (key, value)| {
589 let dirty = short_of(value)
590 .filter(|loser| get_bit(&event_stale, *loser))
591 .zip(RelationKey::try_from(key).ok());
592
593 if let Some((loser, key)) = dirty {
594 relations.push((key, loser));
595 }
596
597 relations
598 })
599 .await;
600
601 warn!(
604 dirty,
605 entries,
606 infected = counts.infected.len(),
607 ghosts = counts.ghosts,
608 removed_ghosts = counts.removed_ghosts,
609 orphans = counts.orphans,
610 missing_parents = counts.missing_parents,
611 infected_parents,
612 malformed_diffs = counts.malformed,
613 moves = moves.len(),
614 relations = relations.len(),
615 "Swept the deeper short id indexes."
616 );
617
618 Scan {
619 dirty,
620 entries,
621 infected: counts.infected,
622 orphans: counts.orphans,
623 missing_parents: counts.missing_parents,
624 infected_parents,
625 malformed_diffs: counts.malformed,
626 moves,
627 relations,
628 ..Default::default()
629 }
630}
631
632async fn diffs(db: &Database, words: usize, walk: Diffs<'_>) -> Counts {
637 let parents = db["shortstatehash_statediff"]
638 .raw_keys()
639 .ignore_err()
640 .ready_fold(vec![0_u64; words], |mut bits, key| {
641 if let Some(short) = short_of(key) {
642 set_bit(&mut bits, short);
643 }
644
645 bits
646 })
647 .await;
648
649 db["shortstatehash_statediff"]
650 .raw_stream()
651 .ignore_err()
652 .ready_fold(Counts::default(), |counts, (key, value)| {
653 walk.row(counts, key, value, &parents)
654 })
655 .await
656}
657
658impl Diffs<'_> {
659 fn row(&self, mut counts: Counts, key: &[u8], value: &[u8], parents: &[u64]) -> Counts {
666 let (Some(row), Some(parent)) = (short_of(key), value.get(0..8).and_then(short_of))
667 else {
668 counts.malformed = counts.malformed.saturating_add(1);
669 return counts;
670 };
671
672 if parent != 0 && parent <= self.counter && !get_bit(parents, parent) {
673 counts.missing_parents = counts.missing_parents.saturating_add(1);
674 }
675
676 let mut removed_run = false;
677 let mut removed = 0_u64;
678 let mut at = 8_usize;
679
680 while at < value.len() {
681 if !removed_run && value[at..].starts_with(&0_u64.to_be_bytes()) {
682 removed_run = true;
683 at = at.saturating_add(8);
684 continue;
685 }
686
687 let entries = (
688 value
689 .get(at..at.saturating_add(8))
690 .and_then(short_of),
691 value
692 .get(at.saturating_add(8)..at.saturating_add(16))
693 .and_then(short_of),
694 );
695
696 let (Some(statekey), Some(event)) = entries else {
697 counts.malformed = counts.malformed.saturating_add(1);
698 return counts;
699 };
700
701 removed = removed.saturating_add(u64::from(removed_run));
702
703 if get_bit(self.statekey_stale, statekey) || get_bit(self.event_stale, event) {
704 counts.infected.insert(row);
705 counts.ghosts = counts.ghosts.saturating_add(1);
706 counts.removed_ghosts = counts
707 .removed_ghosts
708 .saturating_add(u64::from(removed_run));
709 }
710
711 let orphaned = (statekey <= self.counter
712 && !get_bit(self.statekey_reverse, statekey))
713 || (event <= self.counter && !get_bit(self.event_reverse, event));
714
715 counts.orphans = counts.orphans.saturating_add(u64::from(orphaned));
716 at = at.saturating_add(16);
717 }
718
719 if removed_run && removed == 0 {
721 counts.malformed = counts.malformed.saturating_add(1);
722 }
723
724 counts
725 }
726}
727
728#[tracing::instrument(level = "debug", skip_all)]
733async fn strays(db: &Database, counter: u64, words: usize) -> u64 {
734 let rooms = db["roomid_shortroomid"]
735 .raw_stream()
736 .ignore_err()
737 .ready_fold(vec![0_u64; words], |mut bits, (_, value)| {
738 if let Some(short) = short_of(value) {
739 set_bit(&mut bits, short);
740 }
741
742 bits
743 })
744 .await;
745
746 let stray = |short: Option<u64>| {
747 u64::from(short.is_some_and(|short| short <= counter && !get_bit(&rooms, short)))
748 };
749
750 let strays = db["pduid_pdu"]
751 .raw_keys()
752 .ignore_err()
753 .ready_fold(0_u64, |strays, key| {
754 strays.saturating_add(stray(key.get(0..8).and_then(short_of)))
755 })
756 .await;
757
758 let strays = db["tokenids"]
761 .raw_keys()
762 .ignore_err()
763 .ready_fold(strays, |strays, key| {
764 let prefix = key.get(0..8).and_then(short_of);
765 let embedded = key.get(8..).and_then(pdu_shortroomid);
766
767 strays
768 .saturating_add(stray(prefix))
769 .saturating_add(stray(embedded))
770 })
771 .await;
772
773 let current = db["servercurrentevent_data"]
776 .raw_stream()
777 .ignore_err();
778
779 let strays = db["servernameevent_data"]
780 .raw_stream()
781 .ignore_err()
782 .chain(current)
783 .ready_fold(strays, |strays, (key, value)| {
784 let pdu = value.is_empty().and_then(|| pdu_shortroomid(key));
785
786 strays.saturating_add(stray(pdu))
787 })
788 .await;
789
790 db["useridcount_notification"]
791 .raw_stream()
792 .ignore_err()
793 .ready_fold(strays, |strays, (_, value)| {
794 let sroomid = serde_json::from_slice(value)
795 .ok()
796 .map(|notification: Notification| notification.sroomid);
797
798 strays.saturating_add(stray(sroomid))
799 })
800 .await
801}
802
803fn pdu_shortroomid(bytes: &[u8]) -> Option<u64> {
808 let sep = bytes.iter().position(|&byte| byte == SEP)?;
809 let id = bytes.get(sep.saturating_add(1)..)?;
810
811 (id.len() == 16 || id.len() == 24)
812 .and_then(|| id.get(0..8))
813 .and_then(short_of)
814}
815
816pub(super) fn short_of(bytes: &[u8]) -> Option<u64> {
817 bytes.try_into().ok().map(u64::from_be_bytes)
818}
819
820fn bits_of(shorts: &[u64], words: usize) -> Bits {
821 shorts
822 .iter()
823 .fold(vec![0_u64; words], |mut bits, short| {
824 set_bit(&mut bits, *short);
825
826 bits
827 })
828}
829
830fn disposable(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
831 !bytes.len().is_multiple_of(size_of::<u64>())
832 || references(bytes, event_stale, statekey_stale)
833}
834
835fn references(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
836 bytes
837 .as_chunks::<{ size_of::<u64>() }>()
838 .0
839 .iter()
840 .copied()
841 .map(u64::from_be_bytes)
842 .any(|short| get_bit(event_stale, short) || get_bit(statekey_stale, short))
843}
844
845fn set_bit(bits: &mut [u64], index: u64) {
846 if let Some(word) = usize::try_from(index / 64)
847 .ok()
848 .and_then(|word| bits.get_mut(word))
849 {
850 *word |= 1_u64 << (index % 64);
851 }
852}
853
854fn get_bit(bits: &[u64], index: u64) -> bool {
855 usize::try_from(index / 64)
856 .ok()
857 .and_then(|word| bits.get(word))
858 .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
859}
860
861#[cfg(test)]
862mod tests {
863 use super::{Candidate, Family, Identity, by_identity, by_short, contenders};
864
865 fn candidate(short: u64, identity: &[u8]) -> Candidate {
866 (short, Identity::from_slice(identity))
867 }
868
869 #[test]
870 fn contenders_counts_two_forward_rows_claiming_one_short() {
871 let mut dangling = vec![candidate(7, b"$a"), candidate(7, b"$b"), candidate(9, b"$c")];
872
873 assert_eq!(contenders(&mut dangling, by_short), 1);
874 }
875
876 #[test]
877 fn contenders_counts_two_reverse_rows_naming_one_identity() {
878 let mut promotable = vec![candidate(7, b"$a"), candidate(9, b"$a"), candidate(11, b"$b")];
879
880 assert_eq!(contenders(&mut promotable, by_identity), 1);
881 }
882
883 #[test]
884 fn contenders_is_zero_when_every_slot_is_claimed_once() {
885 let mut dangling = vec![candidate(9, b"$a"), candidate(7, b"$b")];
886
887 assert_eq!(contenders(&mut dangling, by_short), 0);
888 }
889
890 #[test]
891 fn a_lone_dangling_winner_heals_without_refusing() {
892 let family = Family {
893 dangling: vec![candidate(7, b"$a")],
894 ..Default::default()
895 };
896
897 assert!(family.healable());
898 assert!(!family.anomalous());
899 }
900
901 #[test]
902 fn a_contended_short_refuses_instead_of_healing() {
903 let family = Family {
904 dangling: vec![candidate(7, b"$a"), candidate(7, b"$b")],
905 contended: 1,
906 ..Default::default()
907 };
908
909 assert!(!family.healable());
910 assert!(family.anomalous());
911 }
912
913 #[test]
914 fn a_malformed_key_withholds_the_heal() {
915 let family = Family {
916 dangling: vec![candidate(7, b"$a")],
917 malformed: 1,
918 ..Default::default()
919 };
920
921 assert!(!family.healable());
922 }
923
924 #[test]
925 fn an_unresolved_row_withholds_the_promotion() {
926 let family = Family {
927 promotable: vec![candidate(7, b"$a")],
928 unresolved: 1,
929 ..Default::default()
930 };
931
932 assert!(!family.healable());
933 }
934}