use std::collections::{HashMap, HashSet, VecDeque};
use crate::primitives::{AccountId, AccountSetId};
use super::error::AccountSetError;
pub(super) const MAX_MEMBERSHIP_DEPTH: i32 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct SetMembership {
pub account_set_id: AccountSetId,
pub member_account_set_id: AccountSetId,
}
impl From<(AccountSetId, AccountSetId)> for SetMembership {
fn from((account_set_id, member_account_set_id): (AccountSetId, AccountSetId)) -> Self {
Self {
account_set_id,
member_account_set_id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct AccountMembership {
pub account_set_id: AccountSetId,
pub account_id: AccountId,
}
impl From<(AccountSetId, AccountId)> for AccountMembership {
fn from((account_set_id, account_id): (AccountSetId, AccountId)) -> Self {
Self {
account_set_id,
account_id,
}
}
}
pub(super) fn has_duplicate_account_membership_paths<'a>(
proposed: &[AccountMembership],
existing: &[AccountMembership],
parents_of: impl Fn(&AccountSetId) -> Option<&'a [AccountSetId]>,
) -> Option<bool> {
let mut per_account: HashMap<AccountId, Vec<AccountSetId>> = HashMap::new();
for membership in proposed.iter().chain(existing) {
per_account
.entry(membership.account_id)
.or_default()
.push(membership.account_set_id);
}
for seeds in per_account.into_values() {
let mut path_counts: HashMap<AccountSetId, u32> = HashMap::new();
let mut queue: VecDeque<AccountSetId> = seeds.into();
while let Some(account_set_id) = queue.pop_front() {
let parents = parents_of(&account_set_id)?;
let count = path_counts.entry(account_set_id).or_default();
*count += 1;
if *count > 1 {
return Some(true);
}
queue.extend(parents);
}
}
Some(false)
}
struct SetDag {
adjacency: HashMap<AccountSetId, Vec<AccountSetId>>,
indegree: HashMap<AccountSetId, usize>,
}
struct Traversal {
order: Vec<AccountSetId>,
depths: HashMap<AccountSetId, i32>,
}
impl Traversal {
fn max_depth(&self) -> i32 {
self.depths.values().copied().max().unwrap_or(0)
}
}
impl SetDag {
fn new<'a>(edges: impl IntoIterator<Item = &'a SetMembership>) -> Self {
let mut adjacency: HashMap<AccountSetId, Vec<AccountSetId>> = HashMap::new();
let mut indegree: HashMap<AccountSetId, usize> = HashMap::new();
for edge in edges {
adjacency
.entry(edge.account_set_id)
.or_default()
.push(edge.member_account_set_id);
*indegree.entry(edge.member_account_set_id).or_default() += 1;
indegree.entry(edge.account_set_id).or_default();
}
Self {
adjacency,
indegree,
}
}
fn add_isolated(&mut self, account_set_id: AccountSetId) {
self.indegree.entry(account_set_id).or_default();
}
fn node_count(&self) -> usize {
self.indegree.len()
}
fn children(&self, account_set_id: &AccountSetId) -> &[AccountSetId] {
self.adjacency
.get(account_set_id)
.map(Vec::as_slice)
.unwrap_or(&[])
}
fn traverse(&self) -> Traversal {
let mut remaining = self.indegree.clone();
let mut queue: VecDeque<AccountSetId> = remaining
.iter()
.filter_map(|(id, degree)| (*degree == 0).then_some(*id))
.collect();
let mut order = Vec::with_capacity(remaining.len());
let mut depths: HashMap<AccountSetId, i32> = HashMap::new();
while let Some(account_set_id) = queue.pop_front() {
order.push(account_set_id);
let parent_depth = *depths.get(&account_set_id).unwrap_or(&0);
for child in self.children(&account_set_id) {
depths
.entry(*child)
.and_modify(|depth| *depth = (*depth).max(parent_depth + 1))
.or_insert(parent_depth + 1);
let degree = remaining
.get_mut(child)
.expect("every child must have an indegree");
*degree -= 1;
if *degree == 0 {
queue.push_back(*child);
}
}
}
Traversal { order, depths }
}
fn has_path(&self, from: AccountSetId, to: AccountSetId) -> bool {
let mut pending = vec![from];
let mut visited = HashSet::new();
while let Some(current) = pending.pop() {
if current == to {
return true;
}
if visited.insert(current) {
pending.extend(self.children(¤t));
}
}
false
}
}
pub(super) fn validate_set_memberships(
existing_edges: &[SetMembership],
proposed_edges: &[SetMembership],
account_members: &[AccountMembership],
) -> Result<(), AccountSetError> {
let mut dag = SetDag::new(existing_edges.iter().chain(proposed_edges));
for membership in account_members {
dag.add_isolated(membership.account_set_id);
}
let traversal = dag.traverse();
if traversal.order.len() != dag.node_count() {
let edge = proposed_edges
.iter()
.find(|edge| {
edge.account_set_id == edge.member_account_set_id
|| dag.has_path(edge.member_account_set_id, edge.account_set_id)
})
.copied()
.or_else(|| existing_edges.first().copied())
.expect("cycle detected in a graph with no edges");
return Err(AccountSetError::MembershipCycleDetected {
account_set_id: edge.account_set_id,
member_account_set_id: edge.member_account_set_id,
});
}
let mut ancestors: HashMap<AccountSetId, HashSet<AccountSetId>> = HashMap::new();
for account_set_id in &traversal.order {
let mut contribution = ancestors.get(account_set_id).cloned().unwrap_or_default();
contribution.insert(*account_set_id);
for child in dag.children(account_set_id) {
let child_ancestors = ancestors.entry(*child).or_default();
if !child_ancestors.is_disjoint(&contribution) {
return Err(AccountSetError::MemberAlreadyAdded);
}
child_ancestors.extend(contribution.iter().copied());
}
}
let mut account_paths = HashSet::new();
for membership in account_members {
if !account_paths.insert(*membership) {
return Err(AccountSetError::MemberAlreadyAdded);
}
if let Some(containers) = ancestors.get(&membership.account_set_id) {
for container in containers {
if !account_paths.insert(AccountMembership {
account_set_id: *container,
account_id: membership.account_id,
}) {
return Err(AccountSetError::MemberAlreadyAdded);
}
}
}
}
if traversal.max_depth() > MAX_MEMBERSHIP_DEPTH {
let (index, depth) = first_depth_overflow(existing_edges, proposed_edges);
let edge = proposed_edges[index];
return Err(AccountSetError::MembershipDepthExceeded {
account_set_id: edge.account_set_id,
member_account_set_id: edge.member_account_set_id,
depth,
max: MAX_MEMBERSHIP_DEPTH,
});
}
Ok(())
}
fn first_depth_overflow(
existing_edges: &[SetMembership],
proposed_edges: &[SetMembership],
) -> (usize, i32) {
let prefix_depth = |take: usize| {
SetDag::new(existing_edges.iter().chain(&proposed_edges[..take]))
.traverse()
.max_depth()
};
let mut low = 1;
let mut high = proposed_edges.len();
while low < high {
let middle = (low + high) / 2;
if prefix_depth(middle) > MAX_MEMBERSHIP_DEPTH {
high = middle;
} else {
low = middle + 1;
}
}
(low - 1, prefix_depth(low))
}
#[cfg(test)]
mod tests {
use super::*;
fn set_ids<const N: usize>() -> [AccountSetId; N] {
std::array::from_fn(|_| AccountSetId::new())
}
fn edge(account_set_id: AccountSetId, member_account_set_id: AccountSetId) -> SetMembership {
SetMembership {
account_set_id,
member_account_set_id,
}
}
fn member(account_set_id: AccountSetId, account_id: AccountId) -> AccountMembership {
AccountMembership {
account_set_id,
account_id,
}
}
#[test]
fn dag_orders_parents_before_members() {
let [root, branch, leaf] = set_ids();
let dag = SetDag::new(&[edge(root, branch), edge(branch, leaf)]);
let order = dag.traverse().order;
let position = |id| order.iter().position(|other| *other == id).unwrap();
assert_eq!(order.len(), dag.node_count());
assert!(position(root) < position(branch));
assert!(position(branch) < position(leaf));
}
#[test]
fn dag_counts_isolated_sets_as_nodes() {
let [root, branch, lone] = set_ids();
let mut dag = SetDag::new(&[edge(root, branch)]);
assert_eq!(dag.node_count(), 2);
dag.add_isolated(lone);
assert_eq!(dag.node_count(), 3);
assert_eq!(dag.traverse().order.len(), 3);
}
#[test]
fn dag_leaves_a_cycles_nodes_out_of_the_order() {
let [a, b, c] = set_ids();
let dag = SetDag::new(&[edge(a, b), edge(b, c), edge(c, a)]);
assert!(dag.traverse().order.len() < dag.node_count());
}
#[test]
fn dag_measures_the_longest_path_not_the_shortest() {
let [root, branch, leaf] = set_ids();
let dag = SetDag::new(&[edge(root, branch), edge(branch, leaf), edge(root, leaf)]);
assert_eq!(dag.traverse().max_depth(), 2);
}
#[test]
fn dag_max_depth_of_an_empty_graph_is_zero() {
assert_eq!(SetDag::new(&[]).traverse().max_depth(), 0);
}
#[test]
fn dag_finds_paths_only_downward() {
let [root, branch, leaf] = set_ids();
let dag = SetDag::new(&[edge(root, branch), edge(branch, leaf)]);
assert!(dag.has_path(root, leaf));
assert!(!dag.has_path(leaf, root));
}
#[test]
fn dag_traversal_is_repeatable() {
let [root, branch] = set_ids();
let dag = SetDag::new(&[edge(root, branch)]);
assert_eq!(dag.traverse().order, dag.traverse().order);
assert_eq!(dag.traverse().max_depth(), 1);
}
#[test]
fn account_paths_accept_distinct_ancestors() {
let [left, right] = set_ids();
let account_id = AccountId::new();
let known = HashSet::from([left, right]);
let parents: HashMap<AccountSetId, Vec<AccountSetId>> = HashMap::new();
assert_eq!(
has_duplicate_account_membership_paths(
&[member(left, account_id), member(right, account_id)],
&[],
|account_set_id| known.contains(account_set_id).then(|| parents
.get(account_set_id)
.map(Vec::as_slice)
.unwrap_or(&[])),
),
Some(false)
);
}
#[test]
fn account_paths_reject_a_shared_ancestor() {
let [root, left, right] = set_ids();
let account_id = AccountId::new();
let known = HashSet::from([root, left, right]);
let parents = HashMap::from([(left, vec![root]), (right, vec![root])]);
assert_eq!(
has_duplicate_account_membership_paths(
&[member(left, account_id), member(right, account_id)],
&[],
|account_set_id| known.contains(account_set_id).then(|| parents
.get(account_set_id)
.map(Vec::as_slice)
.unwrap_or(&[])),
),
Some(true)
);
}
#[test]
fn account_paths_defer_an_unknown_set() {
let [unknown] = set_ids();
assert_eq!(
has_duplicate_account_membership_paths(
&[member(unknown, AccountId::new())],
&[],
|_| None,
),
None
);
}
#[test]
fn set_paths_accept_a_valid_combined_tree() {
let [root, branch, existing_leaf, proposed_leaf] = set_ids();
let existing = [edge(root, branch), edge(branch, existing_leaf)];
let proposed = [edge(branch, proposed_leaf)];
assert!(validate_set_memberships(&existing, &proposed, &[]).is_ok());
}
#[test]
fn set_paths_reject_a_cycle_created_within_the_batch() {
let [a, b, c] = set_ids();
let proposed = [edge(a, b), edge(b, c), edge(c, a)];
assert!(matches!(
validate_set_memberships(&[], &proposed, &[]),
Err(AccountSetError::MembershipCycleDetected { .. })
));
}
#[test]
fn set_paths_reject_a_duplicate_existing_and_proposed_path() {
let [root, branch, leaf] = set_ids();
let existing = [edge(root, branch), edge(branch, leaf)];
let proposed = [edge(root, leaf)];
assert!(matches!(
validate_set_memberships(&existing, &proposed, &[]),
Err(AccountSetError::MemberAlreadyAdded)
));
}
#[test]
fn set_paths_reject_an_account_reachable_twice() {
let [root, left, right] = set_ids();
let account_id = AccountId::new();
let existing = [edge(root, left), edge(root, right)];
let account_members = [member(left, account_id), member(right, account_id)];
assert!(matches!(
validate_set_memberships(&existing, &[], &account_members),
Err(AccountSetError::MemberAlreadyAdded)
));
}
#[test]
fn set_paths_attribute_the_first_depth_overflow() {
let sets: [AccountSetId; 18] = set_ids();
let proposed: Vec<_> = sets.windows(2).map(|pair| edge(pair[0], pair[1])).collect();
assert!(matches!(
validate_set_memberships(&[], &proposed, &[]),
Err(AccountSetError::MembershipDepthExceeded {
account_set_id,
member_account_set_id,
depth: 17,
max: 16,
}) if account_set_id == sets[16] && member_account_set_id == sets[17]
));
}
}