Initial commit sketching out ideas
This commit is contained in:
commit
61f485ea3f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "state-res"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Devin R <devin.ragotzy@gmail.com>"]
|
||||||
|
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"]
|
154
src/lib.rs
Normal file
154
src/lib.rs
Normal file
@ -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<StateMap<EventId>>),
|
||||||
|
Resolved(Vec<StateMap<EventId>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mapping of event type and state_key to some value `T`, usually an `EventId`.
|
||||||
|
pub type StateMap<T> = BTreeMap<(EventType, String), T>;
|
||||||
|
|
||||||
|
/// A mapping of `EventId` to `T`, usually a `StateEvent`.
|
||||||
|
pub type EventMap<T> = BTreeMap<EventId, T>;
|
||||||
|
|
||||||
|
#[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<StateEvent>,
|
||||||
|
/// The resolved state, kept to have easy access to the last resolved
|
||||||
|
/// layer of state.
|
||||||
|
pub state: BTreeMap<EventType, BTreeMap<String, StateEvent>>,
|
||||||
|
/// The graph of authenticated events, kept to find the most recent auth event
|
||||||
|
/// in a chain for incoming state sets.
|
||||||
|
pub auth_graph: BTreeMap<EventId, Vec<StateMap<EventId>>>,
|
||||||
|
/// 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<StateEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<StateMap<EventId>>,
|
||||||
|
store: &mut dyn StateStore,
|
||||||
|
// TODO actual error handling (`thiserror`??)
|
||||||
|
) -> Result<ResolutionResult, serde_json::Error> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
// 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<EventId>],
|
||||||
|
) -> (Vec<StateMap<EventId>>, Vec<StateMap<EventId>>) {
|
||||||
|
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<EventId>],
|
||||||
|
event_map: &EventMap<StateEvent>,
|
||||||
|
store: &mut dyn StateStore,
|
||||||
|
) -> Result<Vec<EventId>, serde_json::Error> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revers_topological_power_sort(
|
||||||
|
&mut self,
|
||||||
|
room_id: &RoomId,
|
||||||
|
power_events: &[EventId],
|
||||||
|
event_map: &EventMap<StateEvent>,
|
||||||
|
store: &mut dyn StateStore,
|
||||||
|
conflicted_set: &[StateMap<EventId>],
|
||||||
|
) -> Vec<StateEvent> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iterative_auth_check(
|
||||||
|
&mut self,
|
||||||
|
room_id: &RoomId,
|
||||||
|
room_version: &RoomVersionId,
|
||||||
|
power_events: &[EventId],
|
||||||
|
unconflicted_state: &[StateMap<EventId>],
|
||||||
|
event_map: &EventMap<StateEvent>,
|
||||||
|
store: &mut dyn StateStore,
|
||||||
|
) -> Vec<StateEvent> {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_power_event(event: &&StateMap<EventId>) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
}
|
49
src/state_event.rs
Normal file
49
src/state_event.rs
Normal file
@ -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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let json = Box::<RawJsonValue>::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)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/state_store.rs
Normal file
26
src/state_store.rs
Normal file
@ -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<Vec<StateEvent>, 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<StateEvent>, Vec<StateEvent>), serde_json::Error>;
|
||||||
|
}
|
20
state.md
Normal file
20
state.md
Normal file
@ -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<Event>,
|
||||||
|
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<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Event>) -> Vec<Event> { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
Now to be totally fair I have no real understanding of state res
|
184
tests/init.rs
Normal file
184
tests/init.rs
Normal file
@ -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<Vec<StateEvent>, 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<StateEvent>, Vec<StateEvent>), 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::<StateEvent>(room_create()).unwrap();
|
||||||
|
let b = from_json_value::<StateEvent>(join_rules()).unwrap();
|
||||||
|
let c = from_json_value::<StateEvent>(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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user