From 5842ddf36e984461549e9db6036f49e358e4bf55 Mon Sep 17 00:00:00 2001 From: Devin R Date: Wed, 22 Jul 2020 23:26:55 -0400 Subject: [PATCH] Working ban_vs_power_level test, add travis.yml, logging --- .travis.yml | 7 + Cargo.toml | 6 +- src/event_auth.rs | 56 +++++-- src/lib.rs | 121 +++++++------- tests/resolve.rs | 1 - tests/{init.rs => state_res.rs} | 270 +++++++++++++++++++------------- 6 files changed, 272 insertions(+), 189 deletions(-) create mode 100644 .travis.yml delete mode 100644 tests/resolve.rs rename tests/{init.rs => state_res.rs} (87%) diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..8f14fa53 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: rust +sudo: false +rust: + - stable + - nightly +script: + - cargo test --all diff --git a/Cargo.toml b/Cargo.toml index 1745e72d..cb14f3cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,12 @@ serde = { version = "1.0.114", features = ["derive"] } serde_json = "1.0.56" tracing = "0.1.16" maplit = "1.0.2" +tracing-subscriber = "0.2.8" [dependencies.ruma] -# git = "https://github.com/ruma/ruma" -path = "../__forks__/ruma/ruma" +git = "https://github.com/DevinR528/ruma" +branch = "pdu-deserialize" +# path = "../__forks__/ruma/ruma" features = ["client-api", "federation-api", "appservice-api"] [dev-dependencies] diff --git a/src/event_auth.rs b/src/event_auth.rs index dffc8065..ecb1f29a 100644 --- a/src/event_auth.rs +++ b/src/event_auth.rs @@ -65,22 +65,26 @@ pub fn auth_check( event: &StateEvent, auth_events: StateMap, ) -> Option { - tracing::debug!("auth_check begingin"); + tracing::info!("auth_check beginning"); + + // don't let power from other rooms be used for auth_event in auth_events.values() { if auth_event.room_id() != event.room_id() { return Some(false); } } - // TODO sig_check is false when called by `iterative_auth_check` + // TODO do_sig_check, do_size_check is false when called by `iterative_auth_check` // Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules // // 1. If type is m.room.create: if event.kind() == EventType::RoomCreate { + tracing::info!("start m.room.create check"); + // domain of room_id must match domain of sender. if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { - return Some(false); + return Some(false); // creation events room id does not match senders } // if content.room_version is present and is not a valid version @@ -97,7 +101,7 @@ pub fn auth_check( return Some(false); } - tracing::debug!("m.room.create event was allowed"); + tracing::info!("m.room.create event was allowed"); return Some(true); } @@ -106,18 +110,25 @@ pub fn auth_check( .get(&(EventType::RoomCreate, "".into())) .is_none() { + tracing::warn!("no m.room.create event in auth chain"); + return Some(false); } // check for m.federate if event.room_id().map(|id| id.server_name()) != Some(event.sender().server_name()) { + tracing::info!("checking federation"); + if !can_federate(&auth_events) { + tracing::warn!("federation not allowed"); + return Some(false); } } // 4. if type is m.room.aliases if event.kind() == EventType::RoomAliases { + tracing::info!("starting m.room.aliases check"); // TODO && room_version "special case aliases auth" ?? if event.state_key().is_none() { return Some(false); // must have state_key @@ -130,18 +141,29 @@ pub fn auth_check( return Some(false); } - tracing::debug!("m.room.aliases event was allowed"); + tracing::info!("m.room.aliases event was allowed"); return Some(true); } if event.kind() == EventType::RoomMember { + tracing::info!("starting m.room.member check"); + if is_membership_change_allowed(event, &auth_events)? { - tracing::debug!("m.room.member event was allowed"); + tracing::info!("m.room.member membership change was allowed"); return Some(true); } + + tracing::info!("m.room.member event was allowed"); + return Some(true); } - if !check_event_sender_in_room(event, &auth_events)? { + if let Some(in_room) = check_event_sender_in_room(event, &auth_events) { + if !in_room { + tracing::warn!("sender not in room"); + return Some(false); + } + } else { + tracing::warn!("sender not in room"); return Some(false); } @@ -153,6 +175,7 @@ pub fn auth_check( } if !can_send_event(event, &auth_events)? { + tracing::warn!("user cannot send event"); return Some(false); } @@ -160,6 +183,7 @@ pub fn auth_check( if !check_power_levels(room_version, event, &auth_events)? { return Some(false); } + tracing::info!("power levels event allowed"); } if event.kind() == EventType::RoomRedaction { @@ -168,7 +192,7 @@ pub fn auth_check( } } - tracing::debug!("allowing event passed all checks"); + tracing::info!("allowing event passed all checks"); Some(true) } @@ -194,7 +218,8 @@ fn is_membership_change_allowed( ) -> Option { let content = event .deserialize_content::() - .ok()?; + .ok() + .unwrap(); let membership = content.membership; // check if this is the room creator joining @@ -210,17 +235,14 @@ fn is_membership_change_allowed( } } - let target_user_id = UserId::try_from(event.state_key()?).ok()?; + let target_user_id = UserId::try_from(event.state_key().unwrap()).ok().unwrap(); // if the server_names are different and federation is NOT allowed - if event.room_id()?.server_name() != target_user_id.server_name() { + if event.room_id().unwrap().server_name() != target_user_id.server_name() { if !can_federate(auth_events) { return Some(false); } } - // TODO according to - // https://github.com/matrix-org/synapse/blob/f2af3e4fc550e7e93be1b0f425c3e9c484b96293/synapse/events/__init__.py#L240 - // sender is the `user_id`? let key = (EventType::RoomMember, event.sender().to_string()); let caller = auth_events.get(&key); @@ -246,7 +268,7 @@ fn is_membership_change_allowed( let user_level = get_user_power_level(event.sender(), auth_events); let target_level = get_user_power_level(event.sender(), auth_events); - // synapse has a not "what to do for default here ##" + // synapse has a not "what to do for default here 50" let ban_level = get_named_level(auth_events, "ban", 50); // TODO clean this up @@ -327,12 +349,14 @@ fn is_membership_change_allowed( } /// Is the event's sender in the room that they sent the event to. +/// +/// A return value of None is not a failure fn check_event_sender_in_room( event: &StateEvent, auth_events: &StateMap, ) -> Option { let mem = auth_events.get(&(EventType::RoomMember, event.sender().to_string()))?; - // TODO this is check_membership + // TODO this is check_membership a helper fn in synapse but it does this Some( mem.deserialize_content::() .ok()? diff --git a/src/lib.rs b/src/lib.rs index 6e241900..d0f18cc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ impl StateResolution { store: &dyn StateStore, // TODO actual error handling (`thiserror`??) ) -> Result { - tracing::debug!("State resolution starting"); + tracing::info!("State resolution starting"); let mut event_map = if let Some(ev_map) = event_map { ev_map @@ -81,7 +81,7 @@ impl StateResolution { return Ok(ResolutionResult::Resolved(clean)); } - tracing::debug!("computing {} conflicting events", conflicting.len()); + tracing::info!("computing {} conflicting events", conflicting.len()); // the set of auth events that are not common across server forks let mut auth_diff = self.get_auth_chain_diff(room_id, &state_sets, &event_map, store)?; @@ -94,7 +94,7 @@ impl StateResolution { .into_iter() .collect::>(); - println!( + tracing::debug!( "FULL CONF {:?}", all_conflicted .iter() @@ -102,7 +102,7 @@ impl StateResolution { .collect::>() ); - tracing::debug!("full conflicted set is {} events", all_conflicted.len()); + tracing::info!("full conflicted set is {} events", all_conflicted.len()); // gather missing events for the event_map let events = store @@ -147,7 +147,8 @@ impl StateResolution { .filter(|id| is_power_event(id, store)) .cloned() .collect::>(); - println!( + + tracing::debug!( "POWER {:?}", power_events .iter() @@ -164,7 +165,7 @@ impl StateResolution { &all_conflicted, ); - println!( + tracing::debug!( "SRTD {:?}", sorted_power_levels .iter() @@ -172,7 +173,7 @@ impl StateResolution { .collect::>() ); - // sequentially auth check each event. + // sequentially auth check each power level event event. let resolved = self.iterative_auth_check( room_id, room_version, @@ -182,7 +183,13 @@ impl StateResolution { store, )?; - println!("AUTHED {:?}", resolved.iter().collect::>()); + tracing::debug!( + "AUTHED {:?}", + resolved + .iter() + .map(|(key, id)| (key, id.to_string())) + .collect::>() + ); // At this point the power_events have been resolved we now have to // sort the remaining events using the mainline of the resolved power level. @@ -196,7 +203,7 @@ impl StateResolution { .cloned() .collect::>(); - println!( + tracing::debug!( "LEFT {:?}", events_to_resolve .iter() @@ -206,14 +213,14 @@ impl StateResolution { let power_event = resolved.get(&(EventType::RoomPowerLevels, "".into())); - println!("PL {:?}", power_event); + tracing::debug!("PL {:?}", power_event); let sorted_left_events = self.mainline_sort(room_id, &events_to_resolve, power_event, &event_map, store); - println!( + tracing::debug!( "SORTED LEFT {:?}", - events_to_resolve + sorted_left_events .iter() .map(ToString::to_string) .collect::>() @@ -244,6 +251,11 @@ impl StateResolution { ) -> (StateMap, StateMap>) { use itertools::Itertools; + tracing::info!( + "seperating {} sets of events into conflicted/unconflicted", + state_sets.len() + ); + let mut unconflicted_state = StateMap::new(); let mut conflicted_state = StateMap::new(); @@ -275,17 +287,8 @@ impl StateResolution { ) -> Result, String> { use itertools::Itertools; - println!( - "{:?}", - state_sets - .iter() - .flat_map(|map| map.values()) - .map(ToString::to_string) - .dedup() - .collect::>() - ); - tracing::debug!("calculating auth chain difference"); + store.auth_chain_diff( room_id, state_sets @@ -321,7 +324,7 @@ impl StateResolution { let mut event_to_pl = BTreeMap::new(); for (idx, event_id) in graph.keys().enumerate() { let pl = self.get_power_level_for_sender(room_id, &event_id, event_map, store); - println!("{}", pl); + tracing::debug!("{} power level {}", event_id.to_string(), pl); event_to_pl.insert(event_id.clone(), pl); @@ -333,7 +336,7 @@ impl StateResolution { } self.lexicographical_topological_sort(&mut graph, |event_id| { - println!("{:?}", event_map.get(event_id).unwrap().origin_server_ts()); + // tracing::debug!("{:?}", event_map.get(event_id).unwrap().origin_server_ts()); let ev = event_map.get(event_id).unwrap(); let pl = event_to_pl.get(event_id).unwrap(); // This return value is the key used for sorting events, @@ -354,6 +357,7 @@ impl StateResolution { where F: Fn(&EventId) -> (i64, SystemTime, Option), { + tracing::info!("starting lexicographical topological sort"); // NOTE: an event that has no incoming edges happened most recently, // and an event that has no outgoing edges happened least recently. @@ -409,7 +413,7 @@ impl StateResolution { sorted.push(node.clone()); } - // println!( + // tracing::debug!( // "{:#?}", // sorted.iter().map(ToString::to_string).collect::>() // ); @@ -423,7 +427,11 @@ impl StateResolution { _event_map: &EventMap, // TODO use event_map over store ?? store: &dyn StateStore, ) -> i64 { + tracing::info!("fetch event senders ({}) power level", event_id.to_string()); + let mut pl = None; + // TODO store.auth_event_ids returns "self" with the event ids is this ok + // event.auth_event_ids does not include its own event id ? for aid in store.auth_event_ids(room_id, &[event_id.clone()]).unwrap() { if let Ok(aev) = store.get_event(&aid) { if aev.is_type_and_key(EventType::RoomPowerLevels, "") { @@ -467,25 +475,26 @@ impl StateResolution { fn iterative_auth_check( &mut self, - room_id: &RoomId, + _room_id: &RoomId, room_version: &RoomVersionId, power_events: &[EventId], unconflicted_state: &StateMap, _event_map: &EventMap, // TODO use event_map over store ?? store: &dyn StateStore, ) -> Result, String> { - tracing::debug!("starting iter auth check"); + tracing::info!("starting iterative auth check"); - let resolved_state = unconflicted_state.clone(); + let mut resolved_state = unconflicted_state.clone(); for (idx, event_id) in power_events.iter().enumerate() { let event = store.get_event(event_id).unwrap(); let mut auth_events = BTreeMap::new(); - for aid in store.auth_event_ids(room_id, &[event_id.clone()]).unwrap() { + for aid in event.auth_event_ids() { if let Ok(ev) = store.get_event(&aid) { - // TODO is None the same as "" for state_key, pretty sure it is NOT - auth_events.insert((ev.kind(), ev.state_key().unwrap_or_default()), ev); + // TODO what to do when no state_key is found ?? + // TODO check "rejected_reason", I'm guessing this is redacted_because for ruma ?? + auth_events.insert((ev.kind(), ev.state_key().unwrap()), ev); } else { tracing::warn!("auth event id for {} is missing {}", aid, event_id); } @@ -502,9 +511,14 @@ impl StateResolution { } if !event_auth::auth_check(room_version, &event, auth_events) - .ok_or("Auth check failed due to deserialization most likely".to_string())? + .ok_or("Auth check failed due to deserialization most likely".to_string()) + .unwrap() { // TODO synapse passes here on AuthError ?? + tracing::warn!("event {} failed the authentication", event_id.to_string()); + } else { + // add event to resolved state map + resolved_state.insert((event.kind(), event.state_key().unwrap()), event_id.clone()); } // We yield occasionally when we're working with large data sets to @@ -527,6 +541,10 @@ impl StateResolution { store: &dyn StateStore, ) -> Vec { tracing::debug!("mainline sort of remaining events"); + // tracing::debug!( + // "{:?}", + // to_sort.iter().map(ToString::to_string).collect::>() + // ); // There can be no EventId's to sort, bail. if to_sort.is_empty() { return vec![]; @@ -565,12 +583,7 @@ impl StateResolution { let mut order_map = BTreeMap::new(); for (idx, ev_id) in to_sort.iter().enumerate() { - let depth = self.get_mainline_depth( - room_id, - event_map.get(ev_id).cloned(), - &mainline_map, - store, - ); + let depth = self.get_mainline_depth(store.get_event(ev_id).ok(), &mainline_map, store); order_map.insert( ev_id, ( @@ -596,36 +609,30 @@ impl StateResolution { fn get_mainline_depth( &mut self, - room_id: &RoomId, mut event: Option, mainline_map: &EventMap, store: &dyn StateStore, ) -> usize { - let mut count = 0; while let Some(sort_ev) = event { + tracing::debug!( + "mainline EVENT ID {}", + sort_ev.event_id().unwrap().to_string() + ); if let Some(id) = sort_ev.event_id() { if let Some(depth) = mainline_map.get(id) { return *depth; } } - let auth_events = if let Some(id) = sort_ev.event_id() { - store.auth_event_ids(room_id, &[id.clone()]).unwrap() - } else { - vec![] - }; - if count < 15 { - println!( - "{:?}", - auth_events - .iter() - .map(ToString::to_string) - .collect::>() - ); - } else { - panic!("{}", sort_ev.event_id().unwrap().to_string()) - } - count += 1; + let auth_events = sort_ev.auth_event_ids(); + tracing::debug!( + "mainline AUTH EV {:?}", + auth_events + .iter() + .map(ToString::to_string) + .collect::>() + ); + event = None; for aid in auth_events { diff --git a/tests/resolve.rs b/tests/resolve.rs deleted file mode 100644 index 8b137891..00000000 --- a/tests/resolve.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/init.rs b/tests/state_res.rs similarity index 87% rename from tests/init.rs rename to tests/state_res.rs index 07dd77d8..b3b8fb64 100644 --- a/tests/init.rs +++ b/tests/state_res.rs @@ -21,6 +21,7 @@ use ruma::{ }; use serde_json::{from_value as from_json_value, json, Value as JsonValue}; use state_res::{ResolutionResult, StateEvent, StateMap, StateResolution, StateStore}; +use tracing_subscriber as tracer; static mut SERVER_TIMESTAMP: i32 = 0; @@ -66,15 +67,18 @@ fn member_content_join() -> JsonValue { .unwrap() } -fn to_pdu_event( +fn to_pdu_event( id: &str, sender: UserId, ev_type: EventType, state_key: Option<&str>, content: JsonValue, - auth_events: &[EventId], - prev_events: &[EventId], -) -> StateEvent { + auth_events: &[S], + prev_events: &[S], +) -> StateEvent +where + S: AsRef, +{ let ts = unsafe { let ts = SERVER_TIMESTAMP; // increment the "origin_server_ts" value @@ -86,6 +90,36 @@ fn to_pdu_event( } else { format!("${}:foo", id) }; + let auth_events = auth_events + .iter() + .map(AsRef::as_ref) + .map(|s| { + EventId::try_from( + if s.contains("$") { + s.to_owned() + } else { + format!("${}:foo", s) + } + .as_str(), + ) + }) + .collect::, _>>() + .unwrap(); + let prev_events = prev_events + .iter() + .map(AsRef::as_ref) + .map(|s| { + EventId::try_from( + if s.contains("$") { + s.to_owned() + } else { + format!("${}:foo", s) + } + .as_str(), + ) + }) + .collect::, _>>() + .unwrap(); let json = if let Some(state_key) = state_key { json!({ @@ -249,102 +283,14 @@ fn INITIAL_EDGES() -> Vec { .unwrap() } -pub struct TestStore(RefCell>); - -#[allow(unused)] -impl StateStore for TestStore { - fn get_events(&self, events: &[EventId]) -> Result, String> { - Ok(self - .0 - .borrow() - .iter() - .filter(|e| events.contains(e.0)) - .map(|(_, s)| s) - .cloned() - .collect()) - } - - fn get_event(&self, event_id: &EventId) -> Result { - self.0 - .borrow() - .get(event_id) - .cloned() - .ok_or(format!("{} not found", event_id.to_string())) - } - - fn auth_event_ids( - &self, - room_id: &RoomId, - event_ids: &[EventId], - ) -> Result, String> { - let mut result = vec![]; - let mut stack = event_ids.to_vec(); - - while !stack.is_empty() { - let ev_id = stack.pop().unwrap(); - if result.contains(&ev_id) { - continue; - } - - result.push(ev_id.clone()); - - let event = self.get_event(&ev_id).unwrap(); - for aid in event.auth_event_ids() { - stack.push(aid); - } - } - - Ok(result) - } - - fn auth_chain_diff( - &self, - room_id: &RoomId, - event_ids: Vec>, - ) -> Result, String> { - use itertools::Itertools; - - println!( - "EVENTS FOR AUTH {:?}", - event_ids - .iter() - .map(|v| v.iter().map(ToString::to_string).collect::>()) - .collect::>() - ); - - let mut chains = vec![]; - for ids in event_ids { - let chain = self - .auth_event_ids(room_id, &ids)? - .into_iter() - .collect::>(); - chains.push(chain); - } - - if let Some(chain) = chains.first() { - let rest = chains.iter().skip(1).flatten().cloned().collect(); - let common = chain.intersection(&rest).collect::>(); - println!( - "COMMON {:?}", - common.iter().map(ToString::to_string).collect::>() - ); - Ok(chains - .iter() - .flatten() - .filter(|id| !common.contains(&id)) - .cloned() - .collect::>() - .into_iter() - .collect()) - } else { - Ok(vec![]) - } - } -} - fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: Vec) { use itertools::Itertools; + // to activate logging use `RUST_LOG=debug cargo t` + tracer::fmt() + .with_env_filter(tracer::EnvFilter::from_default_env()) + .init(); + let mut resolver = StateResolution::default(); // TODO what do we fill this with, everything ?? let store = TestStore(RefCell::new( @@ -363,7 +309,6 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: // 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 INITIAL_EVENTS().values().chain(events) { - println!("{:?}", ev.event_id().unwrap().to_string()); graph.insert(ev.event_id().unwrap().clone(), vec![]); fake_event_map.insert(ev.event_id().unwrap().clone(), ev.clone()); } @@ -393,7 +338,6 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: // TODO is this `key_fn` return correct ?? .lexicographical_topological_sort(&graph, |id| (0, UNIX_EPOCH, Some(id.clone()))) { - println!("{}", node.to_string()); let fake_event = fake_event_map.get(&node).unwrap(); let event_id = fake_event.event_id().unwrap(); @@ -411,7 +355,7 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: .collect::>(); // println!( - // "resolving {:#?}", + // "RESOLVING {:?}", // state_sets // .iter() // .map(|map| map @@ -430,7 +374,17 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: ); match resolved { Ok(ResolutionResult::Resolved(state)) => state, - _ => panic!("resolution for {} failed", node), + Ok(ResolutionResult::Conflicted(state)) => panic!( + "conflicted: {:?}", + state + .iter() + .map(|map| map + .iter() + .map(|(key, id)| (key, id.to_string())) + .collect::>()) + .collect::>() + ), + Err(e) => panic!("resolution for {} failed: {}", node, e), } }; @@ -443,10 +397,10 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: } let auth_types = state_res::auth_types_for_event(fake_event); - println!( - "AUTH TYPES {:?}", - auth_types.iter().map(|(t, id)| (t, id)).collect::>() - ); + // println!( + // "AUTH TYPES {:?}", + // auth_types.iter().map(|(t, id)| (t, id)).collect::>() + // ); let mut auth_events = vec![]; for key in auth_types { @@ -500,14 +454,7 @@ fn do_check(events: &[StateEvent], edges: Vec>, expected_state_ids: .get(&EventId::try_from("$END:foo").unwrap()) .unwrap() .iter() - .filter(|(k, v)| { - println!( - "{:?} == {:?}", - start_state.get(k).map(ToString::to_string), - Some(v.to_string()) - ); - expected_state.contains_key(k) || start_state.get(k) != Some(*v) - }) + .filter(|(k, v)| expected_state.contains_key(k) || start_state.get(k) != Some(*v)) .map(|(k, v)| (k.clone(), v.clone())) .collect::>(); @@ -639,3 +586,100 @@ fn test_lexicographical_sort() { .collect::>() ) } + +// A StateStore implementation for testing +// +// + +/// The test state store. +pub struct TestStore(RefCell>); + +#[allow(unused)] +impl StateStore for TestStore { + fn get_events(&self, events: &[EventId]) -> Result, String> { + Ok(self + .0 + .borrow() + .iter() + .filter(|e| events.contains(e.0)) + .map(|(_, s)| s) + .cloned() + .collect()) + } + + fn get_event(&self, event_id: &EventId) -> Result { + self.0 + .borrow() + .get(event_id) + .cloned() + .ok_or(format!("{} not found", event_id.to_string())) + } + + fn auth_event_ids( + &self, + room_id: &RoomId, + event_ids: &[EventId], + ) -> Result, String> { + let mut result = vec![]; + let mut stack = event_ids.to_vec(); + + // DFS for auth event chain + while !stack.is_empty() { + let ev_id = stack.pop().unwrap(); + if result.contains(&ev_id) { + continue; + } + + result.push(ev_id.clone()); + + let event = self.get_event(&ev_id).unwrap(); + stack.extend(event.auth_event_ids()); + } + + Ok(result) + } + + fn auth_chain_diff( + &self, + room_id: &RoomId, + event_ids: Vec>, + ) -> Result, String> { + use itertools::Itertools; + + // println!( + // "EVENTS FOR AUTH {:?}", + // event_ids + // .iter() + // .map(|v| v.iter().map(ToString::to_string).collect::>()) + // .collect::>() + // ); + + let mut chains = vec![]; + for ids in event_ids { + let chain = self + .auth_event_ids(room_id, &ids)? + .into_iter() + .collect::>(); + chains.push(chain); + } + + if let Some(chain) = chains.first() { + let rest = chains.iter().skip(1).flatten().cloned().collect(); + let common = chain.intersection(&rest).collect::>(); + // println!( + // "COMMON {:?}", + // common.iter().map(ToString::to_string).collect::>() + // ); + Ok(chains + .iter() + .flatten() + .filter(|id| !common.contains(&id)) + .cloned() + .collect::>() + .into_iter() + .collect()) + } else { + Ok(vec![]) + } + } +}