refactor(wip): Replace local state resolution with upstream Ruma's

This commit is contained in:
timedout committed 2026-11-11 14:17:05 +00:00
1 parent 93173313ea
commit 302f535e66
24 files changed
+233 -5780

No files matched your search

+1 -1
View File
@@ -130,7 +130,7 @@ pub enum Error {
#[error(transparent)]
SignatureVerification(#[from] ruma::signatures::VerificationError),
#[error(transparent)]
StateRes(#[from] crate::state_res::Error),
StateRes(#[from] ruma::state_res::Error),
#[error("uiaa")]
Uiaa(ruma::api::client::uiaa::UiaaInfo),
-2
View File
@@ -3,10 +3,8 @@
pub mod event;
pub mod pdu;
pub mod state_key;
pub mod state_res;
pub mod versions;
pub use event::{Event, TypeExt as EventTypeExt};
pub use pdu::{PartialPdu, Pdu, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use state_key::StateKey;
pub use state_res::{StateMap, TypeStateKey};
+35
View File
@@ -73,6 +73,9 @@ pub struct Pdu {
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signatures: Option<Box<RawJsonValue>>,
#[serde(default)]
pub rejected: bool,
}
/// Content hashes of a PDU.
@@ -254,3 +257,35 @@ impl Ord for Pdu {
impl PartialOrd for Pdu {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl ruma::state_res::Event for Pdu {
type Id = OwnedEventId;
fn event_id(&self) -> &Self::Id { &self.event_id }
fn room_id(&self) -> Option<&RoomId> { self.room_id.as_deref() }
fn sender(&self) -> &UserId { self.sender.as_ref() }
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(self.origin_server_ts)
}
fn event_type(&self) -> &TimelineEventType { &self.kind }
fn content(&self) -> &RawJsonValue { &self.content }
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
fn prev_events(&self) -> Box<dyn DoubleEndedIterator<Item = &Self::Id> + '_> {
Box::new(self.prev_events.iter())
}
fn auth_events(&self) -> Box<dyn DoubleEndedIterator<Item = &Self::Id> + '_> {
Box::new(self.auth_events.iter())
}
fn redacts(&self) -> Option<&Self::Id> { self.redacts.as_ref() }
fn rejected(&self) -> bool { self.rejected }
}
-17
View File
@@ -1,17 +0,0 @@
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to
//! deal in the Software without restriction, including without limitation the
//! rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
//! sell copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//! FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
//! IN THE SOFTWARE.
-554
View File
@@ -1,554 +0,0 @@
#[cfg(conduwuit_bench)]
extern crate test;
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use futures::{future, future::ready};
use maplit::{btreemap, hashmap, hashset};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
events::{
StateEventType, TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id, uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use crate::{
matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn lexico_topo_sort(c: &mut test::Bencher) {
let graph = hashmap! {
event_id("l") => hashset![event_id("o")],
event_id("m") => hashset![event_id("n"), event_id("o")],
event_id("n") => hashset![event_id("o")],
event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges
event_id("p") => hashset![event_id("o")],
};
c.iter(|| {
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
});
});
}
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
let mut store = TestStore(hashmap! {});
// build up the DAG
let (state_at_bob, state_at_charlie, _) = store.set_up();
c.iter(|| async {
let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
)
.await
{
| Ok(state) => state,
| Err(e) => panic!("{e}"),
};
});
}
#[cfg(conduwuit_bench)]
#[cfg_attr(conduwuit_bench, bench)]
fn resolve_deeper_event_set(c: &mut test::Bencher) {
let mut inner = INITIAL_EVENTS();
let ban = BAN_STATE_SET();
inner.extend(ban);
let store = TestStore(inner.clone());
let state_set_a = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("MB")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_set_b = [
inner.get(&event_id("CREATE")).unwrap(),
inner.get(&event_id("IJR")).unwrap(),
inner.get(&event_id("IMA")).unwrap(),
inner.get(&event_id("IMB")).unwrap(),
inner.get(&event_id("IMC")).unwrap(),
inner.get(&event_id("IME")).unwrap(),
inner.get(&event_id("PA")).unwrap(),
]
.iter()
.map(|ev| {
(
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
c.iter(|| async {
let state_sets = [&state_set_a, &state_set_b];
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve(
&RoomVersionId::V6,
state_sets.into_iter(),
&auth_chain_sets,
&fetch,
&exists,
)
.await
{
| Ok(state) => state,
| Err(_) => panic!("resolution failed during benchmarking"),
};
});
}
//*/////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION DETAILS AHEAD
//
/////////////////////////////////////////////////////////////////////*/
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
#[allow(unused)]
impl<E: Event + Clone> TestStore<E> {
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id)))
}
/// Returns the events that correspond to the `event_ids` sorted in the same
/// order.
fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result<Vec<E>> {
let mut events = vec![];
for id in event_ids {
events.push(self.get_event(room_id, id)?);
}
Ok(events)
}
/// Returns a Vec of the related auth events to the given `event`.
fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
let mut stack = event_ids;
// DFS for auth event chain
while !stack.is_empty() {
let ev_id = stack.pop().unwrap();
if result.contains(&ev_id) {
continue;
}
result.insert(ev_id.clone());
let event = self.get_event(room_id, ev_id.borrow())?;
stack.extend(event.auth_events().map(ToOwned::to_owned));
}
Ok(result)
}
/// Returns a vector representing the difference in auth chains of the given
/// `events`.
fn auth_chain_diff(
&self,
room_id: &RoomId,
event_ids: Vec<Vec<OwnedEventId>>,
) -> Result<Vec<OwnedEventId>> {
let mut auth_chain_sets = vec![];
for ids in event_ids {
// TODO state store `auth_event_ids` returns self in the event ids
// list when an event returns `auth_event_ids` self is not
// contained
let chain = self
.auth_event_ids(room_id, ids)?
.into_iter()
.collect::<HashSet<_>>();
auth_chain_sets.push(chain);
}
if let Some(first) = auth_chain_sets.first().cloned() {
let common = auth_chain_sets
.iter()
.skip(1)
.fold(first, |a, b| a.intersection(b).cloned().collect::<HashSet<_>>());
Ok(auth_chain_sets
.into_iter()
.flatten()
.filter(|id| !common.contains(id))
.collect())
} else {
Ok(vec![])
}
}
}
impl TestStore<Pdu> {
#[allow(clippy::type_complexity)]
fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().to_string().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
let join_rules = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.clone(), alice_mem.event_id().to_owned()],
&[alice_mem.event_id().to_owned()],
);
self.0
.insert(join_rules.event_id().to_owned(), join_rules.clone());
// Bob and Charlie join at the same time, so there is a fork
// this will be represented in the state_sets when we resolve
let bob_mem = to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(bob_mem.event_id().to_owned(), bob_mem.clone());
let charlie_mem = to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&[cre, join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone());
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
.iter()
.map(|ev| {
(
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|ev| {
(
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
.iter()
.map(|ev| {
(
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
ev.event_id().to_owned(),
)
})
.collect::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${}:foo", id).try_into().unwrap()
}
fn alice() -> &'static UserId { user_id!("@alice:foo") }
fn bob() -> &'static UserId { user_id!("@bob:foo") }
fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
fn ella() -> &'static UserId { user_id!("@ella:foo") }
fn room_id() -> &'static RoomId { room_id!("!test:foo") }
fn member_content_ban() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
where
S: AsRef<str>,
{
// We don't care if the addition happens in order just that it is atomic
// (each event has its own value)
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${}:foo", id)
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
Pdu {
event_id: id.try_into().unwrap(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: String::new() },
signatures: None,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().to_string().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::RoomTopic,
Some(""),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
vec![
to_pdu_event(
"PA",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"], // auth_events
&["START"], // prev_events
),
to_pdu_event(
"PB",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["END"],
),
to_pdu_event(
"MB",
alice(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_ban(),
&["CREATE", "IMA", "PB"],
&["PA"],
),
to_pdu_event(
"IME",
ella(),
TimelineEventType::RoomMember,
Some(ella().as_str()),
member_content_join(),
&["CREATE", "IJR", "PA"],
&["MB"],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
-23
View File
@@ -1,23 +0,0 @@
use serde_json::Error as JsonError;
use thiserror::Error;
/// Represents the various errors that arise when resolving state.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// A deserialization error.
#[error(transparent)]
SerdeJson(#[from] JsonError),
/// The given option or version is unsupported.
#[error("Unsupported room version: {0}")]
Unsupported(String),
/// The given event was not found.
#[error("Not found error: {0}")]
NotFound(String),
/// Invalid fields in the given PDU.
#[error("Invalid PDU: {0}")]
InvalidPdu(String),
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-104
View File
@@ -1,104 +0,0 @@
11/29/2020 BRANCH: timo-spec-comp REV: d2a85669cc6056679ce6ca0fde4658a879ad2b08
lexicographical topological sort
time: [1.7123 us 1.7157 us 1.7199 us]
change: [-1.7584% -1.5433% -1.3205%] (p = 0.00 < 0.05)
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
2 (2.00%) low mild
5 (5.00%) high mild
1 (1.00%) high severe
resolve state of 5 events one fork
time: [10.981 us 10.998 us 11.020 us]
Found 3 outliers among 100 measurements (3.00%)
3 (3.00%) high mild
resolve state of 10 events 3 conflicting
time: [26.858 us 26.946 us 27.037 us]
11/29/2020 BRANCH: event-trait REV: f0eb1310efd49d722979f57f20bd1ac3592b0479
lexicographical topological sort
time: [1.7686 us 1.7738 us 1.7810 us]
change: [-3.2752% -2.4634% -1.7635%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe
resolve state of 5 events one fork
time: [10.643 us 10.656 us 10.669 us]
change: [-4.9990% -3.8078% -2.8319%] (p = 0.00 < 0.05)
Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high severe
resolve state of 10 events 3 conflicting
time: [29.149 us 29.252 us 29.375 us]
change: [-0.8433% -0.3270% +0.2656%] (p = 0.25 > 0.05)
No change in performance detected.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
4/26/2020 BRANCH: fix-test-serde REV:
lexicographical topological sort
time: [1.6793 us 1.6823 us 1.6857 us]
Found 9 outliers among 100 measurements (9.00%)
1 (1.00%) low mild
4 (4.00%) high mild
4 (4.00%) high severe
resolve state of 5 events one fork
time: [9.9993 us 10.062 us 10.159 us]
Found 9 outliers among 100 measurements (9.00%)
7 (7.00%) high mild
2 (2.00%) high severe
resolve state of 10 events 3 conflicting
time: [26.004 us 26.092 us 26.195 us]
Found 16 outliers among 100 measurements (16.00%)
11 (11.00%) high mild
5 (5.00%) high severe
6/30/2021 BRANCH: state-closure REV: 174c3e2a72232ad75b3fb14b3551f5f746f4fe84
lexicographical topological sort
time: [1.5496 us 1.5536 us 1.5586 us]
Found 9 outliers among 100 measurements (9.00%)
1 (1.00%) low mild
1 (1.00%) high mild
7 (7.00%) high severe
resolve state of 5 events one fork
time: [10.319 us 10.333 us 10.347 us]
Found 2 outliers among 100 measurements (2.00%)
2 (2.00%) high severe
resolve state of 10 events 3 conflicting
time: [25.770 us 25.805 us 25.839 us]
Found 7 outliers among 100 measurements (7.00%)
5 (5.00%) high mild
2 (2.00%) high severe
7/20/2021 BRANCH stateres-result REV:
This marks the switch to HashSet/Map
lexicographical topological sort
time: [1.8122 us 1.8177 us 1.8233 us]
change: [+15.205% +15.919% +16.502%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7 outliers among 100 measurements (7.00%)
5 (5.00%) high mild
2 (2.00%) high severe
resolve state of 5 events one fork
time: [11.966 us 12.010 us 12.059 us]
change: [+16.089% +16.730% +17.469%] (p = 0.00 < 0.05)
Performance has regressed.
Found 7 outliers among 100 measurements (7.00%)
3 (3.00%) high mild
4 (4.00%) high severe
resolve state of 10 events 3 conflicting
time: [29.092 us 29.201 us 29.311 us]
change: [+12.447% +12.847% +13.280%] (p = 0.00 < 0.05)
Performance has regressed.
Found 9 outliers among 100 measurements (9.00%)
6 (6.00%) high mild
3 (3.00%) high severe
-241
View File
@@ -1,241 +0,0 @@
use std::collections::BTreeMap;
use ruma::{
Int, OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent},
power_levels::{NotificationPowerLevels, default_power_level},
room_version_rules::{AuthorizationRules, RoomVersionRules},
serde::deserialize_v1_powerlevel,
};
use serde::Deserialize;
use serde_json::{Error, from_str as from_json_str};
use super::{
Result,
serde_backports::{
vec_deserialize_int_powerlevel_values, vec_deserialize_v1_powerlevel_values,
},
};
use crate::error;
#[derive(Deserialize)]
struct IntRoomPowerLevelsEventContent {
#[serde(default = "default_power_level")]
ban: Int,
#[serde(default)]
events: BTreeMap<TimelineEventType, Int>,
#[serde(default)]
events_default: Int,
#[serde(default)]
invite: Int,
#[serde(default = "default_power_level")]
kick: Int,
#[serde(default = "default_power_level")]
redact: Int,
#[serde(default = "default_power_level")]
state_default: Int,
#[serde(default)]
users: BTreeMap<OwnedUserId, Int>,
#[serde(default)]
users_default: Int,
#[serde(default)]
notifications: IntNotificationPowerLevels,
}
impl IntRoomPowerLevelsEventContent {
fn into_room_power_levels_content(
self,
auth_rules: &AuthorizationRules,
) -> RoomPowerLevelsEventContent {
let Self {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users,
users_default,
notifications,
} = self;
let mut pl = RoomPowerLevelsEventContent::new(auth_rules);
pl.ban = ban;
pl.events = events;
pl.events_default = events_default;
pl.invite = invite;
pl.kick = kick;
pl.redact = redact;
pl.state_default = state_default;
pl.users = users;
pl.users_default = users_default;
pl.notifications = notifications.into();
pl
}
}
#[derive(Deserialize)]
struct IntNotificationPowerLevels {
#[serde(default = "default_power_level")]
room: Int,
}
impl Default for IntNotificationPowerLevels {
fn default() -> Self { Self { room: default_power_level() } }
}
impl From<IntNotificationPowerLevels> for NotificationPowerLevels {
fn from(int_notif: IntNotificationPowerLevels) -> Self {
let mut notif = Self::new();
notif.room = int_notif.room;
notif
}
}
#[inline]
pub(crate) fn deserialize_power_levels(
content: &str,
room_version: &RoomVersionRules,
) -> Option<RoomPowerLevelsEventContent> {
if room_version.authorization.integer_power_levels {
deserialize_integer_power_levels(content, &room_version.authorization)
} else {
deserialize_legacy_power_levels(content)
}
}
fn deserialize_integer_power_levels(
content: &str,
auth_rules: &AuthorizationRules,
) -> Option<RoomPowerLevelsEventContent> {
match from_json_str::<IntRoomPowerLevelsEventContent>(content) {
| Ok(content) => Some(content.into_room_power_levels_content(auth_rules)),
| Err(_) => {
error!("m.room.power_levels event is not valid with integer values");
None
},
}
}
fn deserialize_legacy_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
match from_json_str(content) {
| Ok(content) => Some(content),
| Err(_) => {
error!(
"m.room.power_levels event is not valid with integer or string integer values"
);
None
},
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentFields {
#[serde(default, deserialize_with = "vec_deserialize_v1_powerlevel_values")]
pub(crate) users: Vec<(OwnedUserId, Int)>,
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) users_default: Int,
}
impl PowerLevelsContentFields {
pub(crate) fn get_user_power(&self, user_id: &UserId) -> Option<&Int> {
let comparator = |item: &(OwnedUserId, Int)| {
let item: &UserId = &item.0;
item.cmp(user_id)
};
self.users
.binary_search_by(comparator)
.ok()
.and_then(|idx| self.users.get(idx).map(|item| &item.1))
}
}
#[derive(Deserialize)]
struct IntPowerLevelsContentFields {
#[serde(default, deserialize_with = "vec_deserialize_int_powerlevel_values")]
users: Vec<(OwnedUserId, Int)>,
#[serde(default)]
users_default: Int,
}
impl From<IntPowerLevelsContentFields> for PowerLevelsContentFields {
fn from(pl: IntPowerLevelsContentFields) -> Self {
let IntPowerLevelsContentFields { users, users_default } = pl;
Self { users, users_default }
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentInvite {
#[serde(default, deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) invite: Int,
}
#[derive(Deserialize)]
struct IntPowerLevelsContentInvite {
#[serde(default)]
invite: Int,
}
impl From<IntPowerLevelsContentInvite> for PowerLevelsContentInvite {
fn from(pl: IntPowerLevelsContentInvite) -> Self {
let IntPowerLevelsContentInvite { invite } = pl;
Self { invite }
}
}
pub(crate) fn deserialize_power_levels_content_invite(
content: &str,
room_version: &RoomVersionRules,
) -> Result<PowerLevelsContentInvite, Error> {
if room_version.authorization.integer_power_levels {
from_json_str::<IntPowerLevelsContentInvite>(content).map(Into::into)
} else {
from_json_str(content)
}
}
#[derive(Deserialize)]
pub(crate) struct PowerLevelsContentRedact {
#[serde(default = "default_power_level", deserialize_with = "deserialize_v1_powerlevel")]
pub(crate) redact: Int,
}
#[derive(Deserialize)]
pub(crate) struct IntPowerLevelsContentRedact {
#[serde(default = "default_power_level")]
redact: Int,
}
impl From<IntPowerLevelsContentRedact> for PowerLevelsContentRedact {
fn from(pl: IntPowerLevelsContentRedact) -> Self {
let IntPowerLevelsContentRedact { redact } = pl;
Self { redact }
}
}
pub(crate) fn deserialize_power_levels_content_redact(
content: &str,
room_version: &RoomVersionRules,
) -> Result<PowerLevelsContentRedact, Error> {
if room_version.authorization.integer_power_levels {
from_json_str::<IntPowerLevelsContentRedact>(content).map(Into::into)
} else {
from_json_str(content)
}
}
@@ -1,124 +0,0 @@
//! These functions are copied from an old version of Ruma. power_levels.rs uses
//! them to lazily deserialize power level events. Upstream Ruma uses a much
//! more elegant approach in its state resolution code, which we may want
//! to look into at some point.
use std::{fmt, marker::PhantomData};
use ruma::{Int, serde::deserialize_v1_powerlevel};
use serde::{
Deserialize, Deserializer,
de::{MapAccess, Visitor},
};
/// Take a Map with values of either an integer number or a string and
/// deserialize those to integer numbers in a Vec of sorted pairs.
///
/// To be used like this:
/// `#[serde(deserialize_with = "vec_deserialize_v1_powerlevel_values")]`
pub(super) fn vec_deserialize_v1_powerlevel_values<'de, D, T>(
de: D,
) -> Result<Vec<(T, Int)>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
{
#[repr(transparent)]
struct IntWrap(Int);
impl<'de> Deserialize<'de> for IntWrap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_v1_powerlevel(deserializer).map(IntWrap)
}
}
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
impl<T> IntMapVisitor<T> {
fn new() -> Self { Self { _phantom: PhantomData } }
}
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
where
T: Deserialize<'de> + Ord,
{
type Value = Vec<(T, Int)>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with integers or strings as values")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut res = Vec::new();
if let Some(hint) = map.size_hint() {
res.reserve(hint);
}
while let Some((k, IntWrap(v))) = map.next_entry()? {
res.push((k, v));
}
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
Ok(res)
}
}
de.deserialize_map(IntMapVisitor::new())
}
/// Take a Map with integer values and deserialize those to a Vec of sorted
/// pairs
///
/// To be used like this:
/// `#[serde(deserialize_with = "vec_deserialize_int_powerlevel_values")]`
pub(super) fn vec_deserialize_int_powerlevel_values<'de, D, T>(
de: D,
) -> Result<Vec<(T, Int)>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Ord,
{
struct IntMapVisitor<T> {
_phantom: PhantomData<T>,
}
impl<T> IntMapVisitor<T> {
fn new() -> Self { Self { _phantom: PhantomData } }
}
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
where
T: Deserialize<'de> + Ord,
{
type Value = Vec<(T, Int)>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with integers as values")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut res = Vec::new();
if let Some(hint) = map.size_hint() {
res.reserve(hint);
}
while let Some(item) = map.next_entry()? {
res.push(item);
}
res.sort_unstable();
res.dedup_by(|a, b| a.0 == b.0);
Ok(res)
}
}
de.deserialize_map(IntMapVisitor::new())
}
-582
View File
@@ -1,582 +0,0 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
sync::atomic::{AtomicU64, Ordering::SeqCst},
};
use futures::future::ready;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, ServerSignatures,
UserId, event_id,
events::{
TimelineEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
int, room_id,
room_version_rules::RoomVersionRules,
uint, user_id,
};
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use super::auth_types_for_event;
use crate::{
Result, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) async fn do_check(
events: &[Pdu],
edges: Vec<Vec<OwnedEventId>>,
expected_state_ids: Vec<OwnedEventId>,
) {
// To activate logging use `RUST_LOG=debug cargo t`
let init_events = INITIAL_EVENTS();
let mut store = TestStore(
init_events
.values()
.chain(events)
.map(|ev| (ev.event_id().to_owned(), ev.clone()))
.collect(),
);
// This will be lexi_topo_sorted for resolution
let mut graph = HashMap::new();
// This is the same as in `resolve` event_id -> OriginalStateEvent
let mut fake_event_map = HashMap::new();
// Create the DB of events that led up to this point
// TODO maybe clean up some of these clones it is just tests but...
for ev in init_events.values().chain(events) {
graph.insert(ev.event_id().to_owned(), HashSet::new());
fake_event_map.insert(ev.event_id().to_owned(), ev.clone());
}
for pair in INITIAL_EDGES().windows(2) {
if let [a, b] = &pair {
graph
.entry(a.to_owned())
.or_insert_with(HashSet::new)
.insert(b.clone());
}
}
for edge_list in edges {
for pair in edge_list.windows(2) {
if let [a, b] = &pair {
graph
.entry(a.to_owned())
.or_insert_with(HashSet::new)
.insert(b.clone());
}
}
}
// event_id -> Pdu
let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
// event_id -> StateMap<OwnedEventId>
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
// Resolve the current state and add it to the state_at_event map then
// continue on in "time"
for node in super::lexicographical_topological_sort(&graph, &|_id| async {
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
})
.await
.unwrap()
{
let fake_event = fake_event_map.get(&node).unwrap();
let event_id = fake_event.event_id().to_owned();
let prev_events = graph.get(&node).unwrap();
let state_before: StateMap<OwnedEventId> = if prev_events.is_empty() {
HashMap::new()
} else if prev_events.len() == 1 {
state_at_event
.get(prev_events.iter().next().unwrap())
.unwrap()
.clone()
} else {
let state_sets = prev_events
.iter()
.filter_map(|k| state_at_event.get(k))
.collect::<Vec<_>>();
info!(
"{:#?}",
state_sets
.iter()
.map(|map| map
.iter()
.map(|((ty, key), id)| format!("(({ty}{key:?}), {id})"))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
);
let auth_chain_sets: Vec<_> = state_sets
.iter()
.map(|map| {
store
.auth_event_ids(room_id(), map.values().cloned().collect())
.unwrap()
})
.collect();
let event_map = &event_map;
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
let resolved = super::resolve(
&RoomVersionRules::V6,
state_sets,
&auth_chain_sets,
&fetch,
&exists,
)
.await;
match resolved {
| Ok(state) => state,
| Err(e) => panic!("resolution for {node} failed: {e}"),
}
};
let mut state_after = state_before.clone();
let ty = fake_event.event_type();
let key = fake_event.state_key().unwrap();
state_after.insert(ty.with_state_key(key), event_id.to_owned());
let auth_types = auth_types_for_event(
fake_event.event_type(),
fake_event.sender(),
fake_event.state_key(),
fake_event.content(),
&RoomVersionRules::V6,
)
.unwrap();
let mut auth_events = vec![];
for key in auth_types {
if state_before.contains_key(&key) {
auth_events.push(state_before[&key].clone());
}
}
// TODO The event is just remade, adding the auth_events and prev_events
// here the `to_pdu_event` was split into `init` and the fn below,
// could be better
let e = fake_event;
let ev_id = e.event_id();
let event = to_pdu_event(
e.event_id().as_str(),
e.sender(),
e.event_type().clone(),
e.state_key(),
e.content().to_owned(),
&auth_events,
&prev_events.iter().cloned().collect::<Vec<_>>(),
);
// We have to update our store, an actual user of this lib would
// be giving us state from a DB.
store.0.insert(ev_id.to_owned(), event.clone());
state_at_event.insert(node, state_after);
event_map.insert(event_id.to_owned(), store.0.get(ev_id).unwrap().clone());
}
let mut expected_state = StateMap::new();
for node in expected_state_ids {
let ev = event_map.get(&node).unwrap_or_else(|| {
panic!(
"{node} not found in {:?}",
event_map
.keys()
.map(ToString::to_string)
.collect::<Vec<_>>()
)
});
let key = ev.event_type().with_state_key(ev.state_key().unwrap());
expected_state.insert(key, node);
}
let start_state = state_at_event.get(event_id!("$START:foo")).unwrap();
let end_state = state_at_event
.get(event_id!("$END:foo"))
.unwrap()
.iter()
.filter(|(k, v)| {
expected_state.contains_key(k)
|| start_state.get(k) != Some(*v)
// Filter out the dummy messages events.
// These act as points in time where there should be a known state to
// test against.
&& **k != ("m.room.message".into(), "dummy".into())
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<StateMap<OwnedEventId>>();
assert_eq!(expected_state, end_state);
}
#[allow(clippy::exhaustive_structs)]
pub(crate) struct TestStore<E: Event>(pub(crate) HashMap<OwnedEventId, E>);
impl<E: Event + Clone> TestStore<E> {
pub(crate) fn get_event(&self, _: &RoomId, event_id: &EventId) -> Result<E> {
self.0
.get(event_id)
.cloned()
.ok_or_else(|| super::Error::NotFound(format!("{event_id} not found")))
.map_err(Into::into)
}
/// Returns a Vec of the related auth events to the given `event`.
pub(crate) fn auth_event_ids(
&self,
room_id: &RoomId,
event_ids: Vec<OwnedEventId>,
) -> Result<HashSet<OwnedEventId>> {
let mut result = HashSet::new();
let mut stack = event_ids;
// DFS for auth event chain
while let Some(ev_id) = stack.pop() {
if result.contains(&ev_id) {
continue;
}
result.insert(ev_id.clone());
let event = self.get_event(room_id, ev_id.borrow())?;
stack.extend(event.auth_events().map(ToOwned::to_owned));
}
Ok(result)
}
}
// A StateStore implementation for testing
#[allow(clippy::type_complexity)]
impl TestStore<Pdu> {
pub(crate) fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
let create_event = to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
);
let cre = create_event.event_id().to_owned();
self.0.insert(cre.clone(), create_event.clone());
let alice_mem = to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&[cre.clone()],
&[cre.clone()],
);
self.0
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
let join_rules = to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&[cre.clone(), alice_mem.event_id().to_owned()],
&[alice_mem.event_id().to_owned()],
);
self.0
.insert(join_rules.event_id().to_owned(), join_rules.clone());
// Bob and Charlie join at the same time, so there is a fork
// this will be represented in the state_sets when we resolve
let bob_mem = to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().as_str()),
member_content_join(),
&[cre.clone(), join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(bob_mem.event_id().to_owned(), bob_mem.clone());
let charlie_mem = to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&[cre, join_rules.event_id().to_owned()],
&[join_rules.event_id().to_owned()],
);
self.0
.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone());
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
.iter()
.map(|e| {
(e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned())
})
.collect::<StateMap<_>>();
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
.iter()
.map(|e| {
(e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned())
})
.collect::<StateMap<_>>();
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
.iter()
.map(|e| {
(e.event_type().with_state_key(e.state_key().unwrap()), e.event_id().to_owned())
})
.collect::<StateMap<_>>();
(state_at_bob, state_at_charlie, expected)
}
}
pub(crate) fn event_id(id: &str) -> OwnedEventId {
if id.contains('$') {
return id.try_into().unwrap();
}
format!("${id}:foo").try_into().unwrap()
}
pub(crate) fn alice() -> &'static UserId { user_id!("@alice:foo") }
pub(crate) fn bob() -> &'static UserId { user_id!("@bob:foo") }
pub(crate) fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
pub(crate) fn ella() -> &'static UserId { user_id!("@ella:foo") }
pub(crate) fn zara() -> &'static UserId { user_id!("@zara:foo") }
pub(crate) fn room_id() -> &'static RoomId { room_id!("!test:foo") }
pub(crate) fn member_content_ban() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
}
pub(crate) fn member_content_join() -> Box<RawJsonValue> {
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
}
pub(crate) fn to_init_pdu_event(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
) -> Pdu {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${id}:foo")
};
Pdu {
event_id: id.try_into().unwrap(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
signatures: None,
}
}
pub(crate) fn to_pdu_event<S>(
id: &str,
sender: &UserId,
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
where
S: AsRef<str>,
{
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
} else {
format!("${id}:foo")
};
let auth_events = auth_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
let prev_events = prev_events
.iter()
.map(AsRef::as_ref)
.map(event_id)
.collect::<Vec<_>>();
Pdu {
event_id: id.try_into().unwrap(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
signatures: None,
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
),
to_pdu_event(
"IMA",
alice(),
TimelineEventType::RoomMember,
Some(alice().as_str()),
member_content_join(),
&["CREATE"],
&["CREATE"],
),
to_pdu_event(
"IPOWER",
alice(),
TimelineEventType::RoomPowerLevels,
Some(""),
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
&["CREATE", "IMA"],
&["IMA"],
),
to_pdu_event(
"IJR",
alice(),
TimelineEventType::RoomJoinRules,
Some(""),
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
&["CREATE", "IMA", "IPOWER"],
&["IPOWER"],
),
to_pdu_event(
"IMB",
bob(),
TimelineEventType::RoomMember,
Some(bob().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IJR"],
),
to_pdu_event(
"IMC",
charlie(),
TimelineEventType::RoomMember,
Some(charlie().as_str()),
member_content_join(),
&["CREATE", "IJR", "IPOWER"],
&["IMB"],
),
to_pdu_event::<&EventId>(
"START",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
to_pdu_event::<&EventId>(
"END",
charlie(),
TimelineEventType::RoomMessage,
Some("dummy"),
to_raw_json_value(&json!({})).unwrap(),
&[],
&[],
),
]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
vec![to_pdu_event::<&EventId>(
"CREATE",
alice(),
TimelineEventType::RoomCreate,
Some(""),
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
&[],
&[],
)]
.into_iter()
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
vec!["START", "IMC", "IMB", "IJR", "IPOWER", "IMA", "CREATE"]
.into_iter()
.map(event_id)
.collect::<Vec<_>>()
}
+1 -1
View File
@@ -35,7 +35,7 @@ pub use conduwuit_build_metadata as build_metadata;
pub use config::Config;
pub use error::Error;
pub use info::version::*;
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res};
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu};
pub use parking_lot::{
Mutex as SyncMutex, RwLock as SyncRwLock, RwLockReadGuard as SyncRwLockReadGuard,
RwLockWriteGuard as SyncRwLockWriteGuard,
+17
View File
@@ -53,4 +53,21 @@ impl super::Map {
let key = ser::serialize(buf, key).expect("failed to serialize query key");
self.get(key)
}
pub fn qry_blocking<K>(self: &Arc<Self>, key: &K) -> Result<Handle<'_>>
where
K: Serialize + ?Sized + Debug,
{
let mut buf = KeyBuf::new();
self.bqry_blocking(key, &mut buf)
}
pub fn bqry_blocking<K, B>(self: &Arc<Self>, key: &K, buf: &mut B) -> Result<Handle<'_>>
where
K: Serialize + ?Sized + Debug,
B: Write + AsRef<[u8]>,
{
let key = ser::serialize(buf, key).expect("failed to serialize query key");
self.get_blocking(key)
}
}
+5 -10
View File
@@ -1,6 +1,8 @@
use std::collections::{HashMap, hash_map};
use conduwuit::{Err, Event, EventTypeExt, PduEvent, Result, err, warn};
use conduwuit::{
Err, Event, EventTypeExt, PduEvent, Result, debug_info, err, utils::TryFutureExtExt, warn,
};
use ruma::{
OwnedEventId, ServerName, api::federation::authorization::get_event_authorization,
room_version_rules::RoomVersionRules,
@@ -191,15 +193,8 @@ impl super::Service {
},
}
}
if !self
.auth_state_check_4(
&pdu,
room_version_rules,
create_event.as_pdu(),
&auth_events_by_key,
)
.await?
{
if let Some(msg) = self.auth_state_check_4(&pdu, room_version_rules) {
debug_info!("Rejecting auth chain event {event_id}: {msg}");
self.reject_and_persist(&event_id, &pdu.to_canonical_object());
}
}
@@ -195,18 +195,10 @@ impl super::Service {
}
}
if !self
.auth_state_check_4(
&pdu_event,
&room_version_rules,
create_event.as_pdu(),
&auth_events_by_key,
)
.await?
{
if let Some(msg) = self.auth_state_check_4(&pdu_event, &room_version_rules) {
self.reject_and_persist(event_id, &incoming_pdu);
return Err!(Request(Forbidden(debug_warn!(
"Event authorisation fails based on event's claimed auth events"
"Event authorisation fails based on event's claimed auth events: {msg}"
))));
}
+7 -3
View File
@@ -17,10 +17,14 @@ use std::{collections::HashMap, fmt::Write, sync::Arc, time::Instant};
use assign::assign;
use async_trait::async_trait;
use conduwuit::{Err, Error, Event, PduEvent, Result, Server, SyncRwLock, utils::MutexMap};
use conduwuit::{
Err, Error, Event, PduEvent, Result, Server, SyncRwLock,
utils::{MutexMap, TryFutureExtExt},
};
pub use fetch_and_handle_outliers::{
DagBuilderTree, GET_MISSING_EVENTS_MAX_BATCH_SIZE, build_local_dag,
};
use futures::future::ok;
use http::StatusCode;
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedServerName,
@@ -131,8 +135,8 @@ impl Service {
}
/// Fetches a single PDU, returning None if there is an error.
async fn event_fetch(&self, event_id: OwnedEventId) -> Option<PduEvent> {
self.services.timeline.get_pdu(&event_id).await.ok()
fn event_fetch(&self, event_id: &EventId) -> Option<PduEvent> {
self.services.timeline.get_pdu_blocking(event_id).ok()
}
/// Returns a rate-limit error if the requested event had a recent failed
+38 -45
View File
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use base64::Engine;
use conduwuit::{
Err, Event, EventTypeExt, PduEvent, Result, debug, debug::DebugInspect, debug_error,
debug_info, err, info, matrix::StateKey, state_res, trace,
debug_info, err, info, matrix::StateKey, trace,
};
use futures::future::ready;
use ruma::{
@@ -12,6 +12,8 @@ use ruma::{
canonical_json::redact,
events::StateEventType,
room_version_rules::{EventIdFormatVersion, RoomVersionRules},
state_res,
state_res::check_state_independent_auth_rules,
};
use crate::rooms::{
@@ -144,24 +146,21 @@ impl super::Service {
/// Checks PDU check 4: Passes authorisation rules based on the event's auth
/// events ([spec]).
///
/// If the auth check fails, false is returned, otherwise true.
/// If the auth check fails, the error message is returned, otherwise
/// `None`.
///
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
pub async fn auth_state_check_4(
pub fn auth_state_check_4(
&self,
pdu: &PduEvent,
room_version_rules: &RoomVersionRules,
create_event: &PduEvent,
auth_events_by_key: &HashMap<(StateEventType, StateKey), PduEvent>,
) -> Result<bool> {
let state_fetch = |ty: &StateEventType, sk: &str| {
let key = (ty.to_owned(), sk.into());
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
};
state_res::event_auth::auth_check(room_version_rules, pdu, state_fetch, create_event)
.await
.map_err(|e| err!("Event self-authentication failed: {e:?}"))
) -> Option<String> {
match check_state_independent_auth_rules(&room_version_rules.authorization, pdu, |e| {
self.event_fetch(e)
}) {
| Ok(_) => None,
| Err(msg) => Some(msg),
}
}
/// Checks that the event passes PDU check 5, which ensures that the event
@@ -177,7 +176,7 @@ impl super::Service {
room_version_rules: &RoomVersionRules,
create_event: &PduEvent,
origin: &ServerName,
) -> Result<(bool, HashMap<u64, OwnedEventId>)> {
) -> Result<(Option<String>, HashMap<u64, OwnedEventId>)> {
debug!(
event_id = %incoming_pdu.event_id,
"Resolving state at event"
@@ -218,29 +217,27 @@ impl super::Service {
}
trace!(state_events = state_before.len(), "Calculated incoming state");
let state_fetch_state = &state_before;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = self.services.short.get_shortstatekey(&k, &s).await.ok()?;
let event_id = state_fetch_state.get(&shortstatekey)?;
self.services.timeline.get_pdu(event_id).await.ok()
};
debug!(
event_id = %incoming_pdu.event_id,
"Running state-before auth check"
);
// PDU check: 5
let auth_check = state_res::event_auth::auth_check(
room_version_rules,
let state_fetch_state = &state_before;
let auth_result = state_res::check_state_dependent_auth_rules(
&room_version_rules.authorization,
incoming_pdu,
|ty, sk| state_fetch(ty.clone(), sk.into()),
create_event.as_pdu(),
)
.await
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
Ok((auth_check, state_before))
|event_type, state_key| {
let ssk = self
.services
.short
.get_shortstatekey_blocking(event_type, state_key)
.ok()?;
let event_id = state_fetch_state.get(&ssk)?;
self.services.timeline.get_pdu_blocking(event_id).ok()
},
);
Ok((auth_result.err(), state_before))
}
/// Checks that the event passes PDU check 6, which ensures that the event
@@ -253,8 +250,7 @@ impl super::Service {
&self,
incoming_pdu: &PduEvent,
room_version_rules: &RoomVersionRules,
create_event: &PduEvent,
) -> Result<bool> {
) -> Result<(), String> {
debug!(
event_id = %incoming_pdu.event_id,
"Gathering auth events"
@@ -270,25 +266,22 @@ impl super::Service {
incoming_pdu.content(),
room_version_rules,
)
.await?;
let state_fetch = |k: &StateEventType, s: &str| {
let key = k.with_state_key(s);
ready(auth_events.get(&key).map(ToOwned::to_owned))
};
.await
.map_err(|e| e.to_string())?;
debug!(
event_id = %incoming_pdu.event_id,
"Running current state auth check"
);
state_res::event_auth::auth_check(
room_version_rules,
state_res::check_state_dependent_auth_rules(
&room_version_rules.authorization,
incoming_pdu,
state_fetch,
create_event.as_pdu(),
|t, s| {
auth_events
.get(&(t.to_owned(), s.to_owned()))
.map(ToOwned::to_owned)
},
)
.await
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))
}
/// Performs PDU check 7 - does the policy server allow this event.
@@ -1,6 +1,6 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
};
@@ -9,13 +9,21 @@ use conduwuit::{
state_res::{self, StateMap},
trace,
utils::stream::{IterStream, ReadyExt, TryWidebandExt, WidebandExt},
warn,
};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join};
use ruma::{OwnedEventId, RoomId, room_version_rules::RoomVersionRules};
use itertools::cloned;
use ruma::{
EventId, OwnedEventId, RoomId,
room_version_rules::RoomVersionRules,
state_res::{Event, utils::event_id_set::EventIdSet},
};
use crate::rooms::state_compressor::CompressedState;
impl super::Service {
/// Resolves the state based on the incoming fork states with the given room
/// version rules.
pub async fn resolve_state(
&self,
room_id: &RoomId,
@@ -48,7 +56,7 @@ impl super::Service {
.event_ids_iter(room_id, state.values().map(Borrow::borrow))
.try_collect()
})
.try_collect::<Vec<HashSet<OwnedEventId>>>();
.try_collect::<Vec<EventIdSet<OwnedEventId>>>();
let fork_states = fork_states
.iter()
@@ -60,19 +68,19 @@ impl super::Service {
.short
.multi_get_statekey_from_short(shortstatekeys)
.zip(event_ids)
.ready_filter_map(|(ty_sk, id)| Some((ty_sk.ok()?, id)))
.ready_filter_map(|(ty_sk, id)| {
Some((ty_sk.ok().map(|(k, s)| (k, s.to_string()))?, id))
})
.collect()
})
.map(Ok::<_, Error>)
.try_collect::<Vec<StateMap<OwnedEventId>>>();
.try_collect::<Vec<ruma::state_res::StateMap<OwnedEventId>>>();
let (fork_states, auth_chain_sets) = try_join(fork_states, auth_chain_sets).await?;
trace!("Resolving state");
let state = self
.state_resolution(room_version_rules, fork_states.iter(), &auth_chain_sets)
.boxed()
.await?;
let state =
self.state_resolution(room_version_rules, fork_states.as_slice(), auth_chain_sets)?;
trace!("State resolution done.");
let state_events: Vec<_> = state
@@ -98,25 +106,72 @@ impl super::Service {
Ok(Arc::new(new_room_state))
}
pub async fn state_resolution<'a, StateSets>(
/// Actually performs state resolution from the given state sets, auth chain
/// sets, and room version rules.
pub fn state_resolution<'a>(
&'a self,
room_version_rules: &'a RoomVersionRules,
state_sets: StateSets,
auth_chain_sets: &'a [HashSet<OwnedEventId>],
) -> Result<StateMap<OwnedEventId>>
where
StateSets: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send,
{
state_sets: &[ruma::state_res::StateMap<OwnedEventId>],
auth_chain_sets: Vec<EventIdSet<OwnedEventId>>,
) -> Result<ruma::state_res::StateMap<OwnedEventId>> {
let event_fetch = |event_id| self.event_fetch(event_id);
let event_exists = |event_id| self.event_exists(event_id);
state_res::resolve(
room_version_rules,
let calc_subgraph = |ss| self.calculate_conflicted_state_subgraph(ss);
ruma::state_res::resolve(
&room_version_rules.authorization,
&room_version_rules
.state_res
.v2_rules()
.expect("can only use state resolution v2 rules"),
state_sets,
auth_chain_sets,
&event_fetch,
&event_exists,
event_fetch,
calc_subgraph,
)
.map_err(|e| err!(error!("State resolution failed: {e:?}")))
.await
.map_err(|e| err!(error!("state resolution failed: {e:?}")))
}
/// Calculates the conflicted state subgraph for state resolution.
fn calculate_conflicted_state_subgraph<'a>(
&self,
state_set: &ruma::state_res::StateMap<&'a EventId>,
) -> Result<HashSet<&'a EventId>> {
let conflicted_events: HashSet<_> = state_set.values().cloned().collect();
let mut subgraph: HashSet<_> = HashSet::new();
let mut stack: Vec<Vec<_>> = vec![conflicted_events.iter().cloned().collect::<Vec<_>>()];
let mut path: Vec<_> = Vec::new();
let mut seen: HashSet<_> = HashSet::new();
let next_event = |stack: &mut Vec<Vec<_>>, path: &mut Vec<_>| {
while stack.last().is_some_and(Vec::is_empty) {
stack.pop();
path.pop();
}
stack.last_mut().and_then(Vec::pop)
};
while let Some(event_id) = next_event(&mut stack, &mut path) {
path.push(event_id.clone());
if subgraph.contains(&event_id) {
if path.len() > 1 {
subgraph.extend(path.iter().cloned());
}
path.pop();
continue;
}
if conflicted_events.contains(&event_id) && path.len() > 1 {
subgraph.extend(path.iter().cloned());
}
if seen.contains(&event_id) {
path.pop();
continue;
}
trace!(event_id = event_id.as_str(), "fetching event for its auth events");
let Some(evt) = self.event_fetch(event_id.clone()) else {
warn!("could not fetch event {} to calculate conflicted subgraph", event_id);
path.pop();
continue;
};
stack.push(evt.auth_events().collect());
seen.insert(event_id);
}
Ok(subgraph)
}
}
@@ -65,14 +65,14 @@ impl super::Service {
// perform PDU check 5 (event auth passes based on state before the
// event). To do this, we either need to have all the prev events
// locally, or ask a remote server for the state at the event.
let (passes_state_before, state_before) = self
let (state_before_failure_reason, state_before) = self
.state_before_check_5(&incoming_pdu, &room_version_rules, create_event, origin)
.await?;
if !passes_state_before {
if let Some(msg) = state_before_failure_reason {
self.reject_and_persist(incoming_pdu.event_id(), &val);
return Err!(Request(Forbidden(debug_warn!(
"Event authorisation fails based on the state before the event"
"Event authorisation fails based on the state before the event: {msg}"
))));
}
@@ -90,16 +90,16 @@ impl super::Service {
);
let state_lock = self.services.state.mutex.lock(room_id).await;
let passes_current_state = self
.current_state_check_6(&incoming_pdu, &room_version_rules, create_event)
.current_state_check_6(&incoming_pdu, &room_version_rules)
.await
.inspect(|passes| {
if !*passes {
debug_warn!(
"Event authorisation fails based on the current room state - will be \
soft-failed"
);
}
})?;
.inspect_err(|reason| {
debug_warn!(
%reason,
"Event authorisation fails based on the current room state - will be \
soft-failed"
);
})
.is_ok();
// Determine whether this PDU should be soft-failed.
// If the auth check failed, invariably yes. Otherwise, only if the user
+9
View File
@@ -154,6 +154,15 @@ impl Service {
.deserialized()
}
pub fn get_shortstatekey_blocking(
&self,
event_type: &StateEventType,
state_key: &str,
) -> Result<ShortStateKey> {
let key = (event_type, state_key);
self.db.shortstatekey_statekey.qry()
}
/// Gets a full event ID from a short event ID.
pub async fn get_eventid_from_short<Id>(&self, shorteventid: ShortEventId) -> Result<Id>
where
+6 -5
View File
@@ -1,11 +1,10 @@
use std::{collections::HashMap, fmt::Write, sync::Arc};
use async_trait::async_trait;
use conduwuit::{debug, utils::stream::WidebandExt};
use conduwuit::{debug, matrix::StateKey, smallstr::SmallString, utils::stream::WidebandExt};
use conduwuit_core::{
Event, PduEvent, Result, err,
result::FlatOk,
state_res::{self, StateMap},
utils::{
IterStream, MutexMap, MutexMapGuard, ReadyExt, calculate_hash,
stream::{BroadbandExt, TryIgnore},
@@ -21,6 +20,7 @@ use ruma::{
api::federation::membership::RawStrippedState,
events::{StateEventType, TimelineEventType, room::create::RoomCreateEventContent},
room_version_rules::RoomVersionRules,
state_res,
};
use crate::{
@@ -472,7 +472,7 @@ impl Service {
state_key: Option<&str>,
content: &serde_json::value::RawValue,
room_version_rules: &RoomVersionRules,
) -> Result<StateMap<PduEvent>> {
) -> Result<HashMap<(StateEventType, String), PduEvent>> {
let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else {
return Ok(HashMap::new());
};
@@ -482,8 +482,9 @@ impl Service {
sender,
state_key,
content,
room_version_rules,
)?;
&room_version_rules.authorization,
)
.map_err(|e| err!("failed to select auth types for event: {e:?}"))?;
debug!(?auth_types, "Auth types for event");
let sauthevents: HashMap<_, _> = auth_types
.iter()
+16
View File
@@ -161,6 +161,22 @@ impl Data {
select_ok([accepted, outlier]).await.map(at!(0))
}
pub(super) fn get_pdu_blocking(&self, event_id: &EventId) -> Result<PduEvent> {
if let Ok(outlier) = self
.eventid_outlierpdu
.get_blocking(event_id)?
.deserialized()
{
return Ok(outlier);
};
let pdu_id = self
.eventid_pduid
.get_blocking(event_id)
.map(|h| RawPduId::from(&*h))?;
self.pduid_pdu.get_blocking(&pdu_id)?.deserialized()
}
/// Like get_non_outlier_pdu(), but without the expense of fetching and
/// parsing the PduEvent
#[inline]
+6
View File
@@ -211,6 +211,12 @@ impl Service {
self.db.get_pdu(event_id).await
}
/// Returns the PDU. Checks the outlier tree BEFORE the timeline tree, as
/// this operation is synchronous.
pub fn get_pdu_blocking(&self, event_id: &EventId) -> Result<PduEvent> {
self.db.get_pdu_blocking(event_id)
}
/// Returns the pdu.
///
/// This does __NOT__ check the outliers `Tree`.