commit 61f485ea3f96044ea341cba9bdd17fb149b00fe3 Author: Devin R Date: Fri Jul 17 10:07:03 2020 -0400 Initial commit sketching out ideas diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..5f77a127 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "state-res" +version = "0.1.0" +authors = ["Devin R "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +petgraph = "0.5.1" +serde = { version = "1.0.114", features = ["derive"] } +serde_json = "1.0.56" + +[dependencies.ruma] +git = "https://github.com/ruma/ruma" +features = ["client-api", "federation-api", "appservice-api"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..fa2a6c50 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,154 @@ +use std::{collections::BTreeMap, time::SystemTime}; + +use petgraph::Graph; +use ruma::{ + events::{ + room::{self}, + AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType, + }, + identifiers::{EventId, RoomId, RoomVersionId}, +}; +use serde::{Deserialize, Serialize}; + +mod state_event; +mod state_store; + +pub use state_event::StateEvent; +pub use state_store::StateStore; + +pub enum ResolutionResult { + Conflicted(Vec>), + Resolved(Vec>), +} + +/// A mapping of event type and state_key to some value `T`, usually an `EventId`. +pub type StateMap = BTreeMap<(EventType, String), T>; + +/// A mapping of `EventId` to `T`, usually a `StateEvent`. +pub type EventMap = BTreeMap; + +#[derive(Debug, Default, Deserialize, Serialize)] // TODO make the ser/de impls useful +pub struct StateResolution { + // TODO remove pub after initial testing + /// The set of resolved events over time. + pub resolved_events: Vec, + /// The resolved state, kept to have easy access to the last resolved + /// layer of state. + pub state: BTreeMap>, + /// The graph of authenticated events, kept to find the most recent auth event + /// in a chain for incoming state sets. + pub auth_graph: BTreeMap>>, + /// The last known point in the state graph. + pub most_recent_resolved: Option<(EventType, String)>, + + // fields for temp storage during resolution + pub conflicting_events: Vec, +} + +impl StateResolution { + /// Resolve sets of state events as they come in. Internally `StateResolution` builds a graph + /// and an auth chain to allow for state conflict resolution. + pub fn resolve( + &mut self, + room_id: &RoomId, + room_version: &RoomVersionId, + state_sets: Vec>, + store: &mut dyn StateStore, + // TODO actual error handling (`thiserror`??) + ) -> Result { + let mut event_map = EventMap::new(); + // split non-conflicting and conflicting state + let (clean, mut conflicting) = self.seperate(&state_sets); + + if conflicting.is_empty() { + return Ok(ResolutionResult::Resolved(clean)); + } + + // the set of auth events that are not common across server forks + let mut auth_diff = self.get_auth_chain_diff(&state_sets, &mut event_map, store)?; + + // add the auth_diff to conflicting now we have a full set of conflicting events + auth_diff.extend(conflicting.iter().flat_map(|map| map.values().cloned())); + let all_conflicted = auth_diff; + + let all_conflicted = conflicting; + + let power_events = all_conflicted + .iter() + .filter(is_power_event) + .flat_map(|map| map.values()) + .cloned() + .collect::>(); + + // sort the power events based on power_level/clock/event_id and outgoing/incoming edges + let sorted_power_levels = self.revers_topological_power_sort( + room_id, + &power_events, + &mut event_map, + store, + &all_conflicted, + ); + + // sequentially auth check each event. + let resolved = self.iterative_auth_check( + room_id, + room_version, + &power_events, + &clean, + &mut event_map, + store, + ); + + // TODO return something not a place holder + Ok(ResolutionResult::Resolved(vec![])) + } + + fn seperate( + &mut self, + state_sets: &[StateMap], + ) -> (Vec>, Vec>) { + panic!() + } + + /// Returns a Vec of deduped EventIds that appear in some chains but no others. + fn get_auth_chain_diff( + &mut self, + state_sets: &[StateMap], + event_map: &EventMap, + store: &mut dyn StateStore, + ) -> Result, serde_json::Error> { + panic!() + } + + fn revers_topological_power_sort( + &mut self, + room_id: &RoomId, + power_events: &[EventId], + event_map: &EventMap, + store: &mut dyn StateStore, + conflicted_set: &[StateMap], + ) -> Vec { + panic!() + } + + fn iterative_auth_check( + &mut self, + room_id: &RoomId, + room_version: &RoomVersionId, + power_events: &[EventId], + unconflicted_state: &[StateMap], + event_map: &EventMap, + store: &mut dyn StateStore, + ) -> Vec { + panic!() + } +} + +pub fn is_power_event(event: &&StateMap) -> bool { + true +} + +#[cfg(test)] +mod tests { + use super::*; +} diff --git a/src/state_event.rs b/src/state_event.rs new file mode 100644 index 00000000..a42c1109 --- /dev/null +++ b/src/state_event.rs @@ -0,0 +1,49 @@ +use ruma::events::{ + from_raw_json_value, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventDeHelper, +}; +use serde::{de, Serialize}; +use serde_json::value::RawValue as RawJsonValue; + +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum StateEvent { + Full(AnyStateEvent), + Sync(AnySyncStateEvent), + Stripped(AnyStrippedStateEvent), +} + +impl<'de> de::Deserialize<'de> for StateEvent { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = Box::::deserialize(deserializer)?; + let EventDeHelper { + state_key, + event_id, + room_id, + unsigned, + .. + } = from_raw_json_value(&json)?; + + // Determine whether the event is a full, sync, or stripped + // based on the fields present. + if room_id.is_some() { + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + panic!("TODO deal with redacted events") + } + _ => StateEvent::Full(from_raw_json_value(&json)?), + }) + } else if event_id.is_some() { + Ok(match unsigned { + Some(unsigned) if unsigned.redacted_because.is_some() => { + panic!("TODO deal with redacted events") + } + _ => StateEvent::Sync(from_raw_json_value(&json)?), + }) + } else { + Ok(StateEvent::Stripped(from_raw_json_value(&json)?)) + } + } +} diff --git a/src/state_store.rs b/src/state_store.rs new file mode 100644 index 00000000..f84e8127 --- /dev/null +++ b/src/state_store.rs @@ -0,0 +1,26 @@ +use std::{collections::BTreeMap, time::SystemTime}; + +use petgraph::Graph; +use ruma::{ + events::{ + room::{self}, + AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType, + }, + identifiers::{EventId, RoomId, RoomVersionId}, +}; + +use crate::StateEvent; + +pub trait StateStore { + /// Returns the events that correspond to the `event_ids` sorted in the same order. + fn get_events(&self, event_ids: &[EventId]) -> Result, serde_json::Error>; + + /// Returns a tuple of requested state events from `event_id` and the auth chain events that + /// relate to the. + fn get_remote_state_for_room( + &self, + room_id: &RoomId, + version: &RoomVersionId, + event_id: &EventId, + ) -> Result<(Vec, Vec), serde_json::Error>; +} diff --git a/state.md b/state.md new file mode 100644 index 00000000..cc5e8575 --- /dev/null +++ b/state.md @@ -0,0 +1,20 @@ +Would it be possible to abstract state res into a `ruma-state-res` crate? I've been thinking about something along the lines of +```rust +// The would need to be Serialize/Deserialize to save state +struct StateResV2 { + resolved_events: Vec, + state_graph: of indexes into the events field?, + most_recent_resolved: index or ptr into the graph?, + // fields for temp storage during resolution + conflicting_events: Vec, +} + +impl StateResV2 { + /// The point of this all add nonconflicting events to the graph + /// and resolve and add conflicting events. + fn resolve(&mut self, events: Vec) -> Vec { } + +} + +``` +Now to be totally fair I have no real understanding of state res diff --git a/tests/init.rs b/tests/init.rs new file mode 100644 index 00000000..93efbc12 --- /dev/null +++ b/tests/init.rs @@ -0,0 +1,184 @@ +use std::convert::TryFrom; + +use ruma::{ + events::{ + room::{self}, + AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventType, + }, + identifiers::{EventId, RoomId, RoomVersionId}, +}; +use serde_json::{from_value as from_json_value, json, Value as JsonValue}; +use state_res::{ResolutionResult, StateEvent, StateResolution, StateStore}; + +// TODO make this an array of events +fn federated_json() -> JsonValue { + json!({ + "content": { + "creator": "@example:example.org", + "m.federate": true, + "predecessor": { + "event_id": "$something:example.org", + "room_id": "!oldroom:example.org" + }, + "room_version": "6" + }, + "event_id": "$aaa:example.org", + "origin_server_ts": 1, + "room_id": "!room_id:example.org", + "sender": "@alice:example.org", + "state_key": "", + "type": "m.room.create", + "unsigned": { + "age": 1234 + } + }) +} + +fn room_create() -> JsonValue { + json!({ + "content": { + "creator": "@example:example.org", + "m.federate": true, + "predecessor": { + "event_id": "$something:example.org", + "room_id": "!oldroom:example.org" + }, + "room_version": "6" + }, + "event_id": "$aaa:example.org", + "origin_server_ts": 1, + "room_id": "!room_id:example.org", + "sender": "@alice:example.org", + "state_key": "", + "type": "m.room.create", + "unsigned": { + "age": 1234 + } + }) +} + +fn join_rules() -> JsonValue { + json!({ + "content": { + "join_rule": "public" + }, + "event_id": "$bbb:example.org", + "origin_server_ts": 2, + "room_id": "!room_id:example.org", + "sender": "@alice:example.org", + "state_key": "", + "type": "m.room.join_rules", + "unsigned": { + "age": 1234 + } + }) +} + +fn join_event() -> JsonValue { + json!({ + "content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "event_id": "$ccc:example.org", + "membership": "join", + "room_id": "!room_id:example.org", + "origin_server_ts": 3, + "sender": "@alice:example.org", + "state_key": "@alice:example.org", + "type": "m.room.member", + "unsigned": { + "age": 1, + "replaces_state": "$151800111315tsynI:example.org", + "prev_content": { + "avatar_url": null, + "displayname": "example", + "membership": "invite" + } + } + }) +} + +fn power_levels() -> JsonValue { + json!({ + "content": { + "ban": 50, + "events": { + "m.room.name": 100, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": { + "room": 20 + }, + "redact": 50, + "state_default": 50, + "users": { + "@example:example.org": 100 + }, + "users_default": 0 + }, + "event_id": "$ddd:example.org", + "origin_server_ts": 4, + "room_id": "!room_id:example.org", + "sender": "@example:example.org", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 1234 + } + }) +} + +pub struct TestStore; + +impl StateStore for TestStore { + fn get_events(&self, events: &[EventId]) -> Result, serde_json::Error> { + Ok(vec![from_json_value(power_levels())?]) + } + + fn get_remote_state_for_room( + &self, + room_id: &RoomId, + version: &RoomVersionId, + event_id: &EventId, + ) -> Result<(Vec, Vec), serde_json::Error> { + Ok(( + vec![from_json_value(federated_json())?], + vec![from_json_value(power_levels())?], + )) + } +} + +#[test] +fn it_works() { + let mut store = TestStore; + + let room_id = RoomId::try_from("!room_id:example.org").unwrap(); + let room_version = RoomVersionId::version_6(); + + let a = from_json_value::(room_create()).unwrap(); + let b = from_json_value::(join_rules()).unwrap(); + let c = from_json_value::(join_event()).unwrap(); + + let mut resolver = StateResolution::default(); + + let res = resolver + .resolve(&room_id, &room_version, vec![a.clone()], &mut store) + .unwrap(); + assert!(if let ResolutionResult::Resolved(_) = res { + true + } else { + false + }); + + let resolved = resolver + .resolve(&room_id, &room_version, vec![b, c], &mut store) + .unwrap(); + + assert!(resolver.conflicting_events.is_empty()); + assert_eq!(resolver.resolved_events.len(), 3); +}