use super::*;
use itertools::Itertools;
pub fn find_minimal_blocking_sets(fbas: &Fbas) -> Vec<NodeIdSet> {
info!("Starting to look for minimal blocking_sets...");
let minimal_blocking_sets = find_minimal_sets(fbas, minimal_blocking_sets_finder);
info!(
"Found {} minimal blocking_sets.",
minimal_blocking_sets.len()
);
minimal_blocking_sets
}
fn minimal_blocking_sets_finder(consensus_clusters: Vec<NodeIdSet>, fbas: &Fbas) -> Vec<NodeIdSet> {
let mut found_blocking_sets_per_cluster: Vec<Vec<NodeIdSet>> = vec![];
for (i, nodes) in consensus_clusters.into_iter().enumerate() {
debug!("Finding minimal blocking sets in cluster {}...", i);
if let Some(symmetric_cluster) =
is_symmetric_cluster(&nodes, &fbas.with_standard_form_quorum_sets())
{
debug!("Cluster contains a symmetric quorum cluster! Extracting blocking sets...");
found_blocking_sets_per_cluster.push(symmetric_cluster.to_minimal_blocking_sets(fbas));
} else {
debug!("Sorting nodes by rank...");
let sorted_nodes = sort_by_rank(nodes.iter().collect(), fbas);
debug!("Sorted.");
debug!("Looking for symmetric nodes...");
let symmetric_nodes = find_symmetric_nodes_in_node_set(&nodes, fbas);
debug!("Done.");
let mut found_unexpanded_blocking_sets_in_this_cluster: Vec<NodeIdSet> = vec![];
debug!("Collecting blocking_sets...");
minimal_blocking_sets_finder_step(
&mut CandidateValues::new(sorted_nodes),
&mut found_unexpanded_blocking_sets_in_this_cluster,
&FbasValues::new(fbas, &symmetric_nodes),
true,
);
let found_blocking_sets =
symmetric_nodes.expand_sets(found_unexpanded_blocking_sets_in_this_cluster);
found_blocking_sets_per_cluster.push(found_blocking_sets);
}
}
found_blocking_sets_per_cluster
.into_iter()
.map(|blocking_sets_group| blocking_sets_group.into_iter())
.multi_cartesian_product()
.map(|blocking_set_combinations| {
let mut combined_blocking_set = bitset![];
for blocking_set in blocking_set_combinations.into_iter() {
combined_blocking_set.union_with(&blocking_set);
}
combined_blocking_set
})
.collect()
}
fn minimal_blocking_sets_finder_step(
candidates: &mut CandidateValues,
found_blocking_sets: &mut Vec<NodeIdSet>,
fbas_values: &FbasValues,
selection_changed: bool,
) {
if selection_changed && is_blocked_set(&candidates.remaining, fbas_values.fbas) {
if is_minimal_for_blocking_set_with_precomputed_blocked_set(
&candidates.selection,
&candidates.remaining,
fbas_values.fbas,
) {
found_blocking_sets.push(candidates.selection.clone());
if found_blocking_sets.len() % 100_000 == 0 {
debug!("...{} blocking_sets found", found_blocking_sets.len());
}
}
} else if let Some(current_candidate) = candidates.unprocessed.pop_front() {
if fbas_values
.symmetric_nodes
.is_non_redundant_next(current_candidate, &candidates.selection)
{
candidates.selection.insert(current_candidate);
candidates.remaining.remove(current_candidate);
minimal_blocking_sets_finder_step(candidates, found_blocking_sets, fbas_values, true);
candidates.selection.remove(current_candidate);
candidates.remaining.insert(current_candidate);
}
candidates.max_remaining.insert(current_candidate);
if is_blocked_set(&candidates.max_remaining, fbas_values.fbas) {
minimal_blocking_sets_finder_step(candidates, found_blocking_sets, fbas_values, false);
}
candidates.unprocessed.push_front(current_candidate);
candidates.max_remaining.remove(current_candidate);
}
}
#[derive(Debug, Clone)]
struct CandidateValues {
selection: NodeIdSet,
unprocessed: NodeIdDeque,
remaining: NodeIdSet,
max_remaining: NodeIdSet,
}
impl CandidateValues {
fn new(sorted_nodes_to_process: Vec<NodeId>) -> Self {
let selection = bitset![];
let unprocessed: NodeIdDeque = sorted_nodes_to_process.into();
let remaining: NodeIdSet = unprocessed.iter().copied().collect();
let max_remaining = bitset![];
Self {
selection,
unprocessed,
remaining,
max_remaining,
}
}
}
#[derive(Debug, Clone)]
struct FbasValues<'a> {
fbas: &'a Fbas,
symmetric_nodes: &'a SymmetricNodesMap,
}
impl<'a> FbasValues<'a> {
fn new(fbas: &'a Fbas, symmetric_nodes: &'a SymmetricNodesMap) -> Self {
Self {
fbas,
symmetric_nodes,
}
}
}
impl QuorumSet {
fn to_minimal_blocking_sets(&self, fbas: &Fbas) -> Vec<NodeIdSet> {
let blocking_sets = self.to_blocking_sets();
if self.contains_duplicates() {
remove_non_minimal_x(blocking_sets, is_minimal_for_blocking_set, fbas)
} else {
blocking_sets
}
}
fn to_blocking_sets(&self) -> Vec<NodeIdSet> {
self.to_slices(|qset| qset.blocking_threshold())
}
fn blocking_threshold(&self) -> usize {
(self.validators.len() + self.inner_quorum_sets.len() + 1).wrapping_sub(self.threshold)
}
}
fn is_minimal_for_blocking_set(blocking_set: &NodeIdSet, fbas: &Fbas) -> bool {
let mut blocked_set = fbas.all_nodes();
blocked_set.difference_with(blocking_set);
is_minimal_for_blocking_set_with_precomputed_blocked_set(blocking_set, blocking_set, fbas)
}
fn is_minimal_for_blocking_set_with_precomputed_blocked_set(
blocking_set: &NodeIdSet,
blocked_set: &NodeIdSet,
fbas: &Fbas,
) -> bool {
let mut tester = blocked_set.clone();
for node_id in blocking_set.iter() {
tester.insert(node_id);
if is_blocked_set(&tester, fbas) {
return false;
}
tester.remove(node_id);
}
true
}
fn is_blocked_set(nodes: &NodeIdSet, fbas: &Fbas) -> bool {
!contains_quorum(nodes, fbas)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn minimal_blocking_sets_in_correct() {
let fbas = Fbas::from_json_file(Path::new("test_data/correct.json"));
let expected = vec![bitset![0, 1], bitset![0, 10], bitset![1, 10]];
let actual = find_minimal_blocking_sets(&fbas);
assert_eq!(expected, actual);
}
#[test]
fn minimal_blocking_sets_in_broken_trivial() {
let fbas = Fbas::from_json_file(Path::new("test_data/broken_trivial.json"));
let expected = vec![bitset![0, 1], bitset![0, 2]];
let actual = find_minimal_blocking_sets(&fbas);
assert_eq!(expected, actual);
}
#[test]
fn flat_2_of_3_quorum_set_to_blocking_sets() {
let qset = QuorumSet {
threshold: 2,
validators: vec![0, 1, 2],
inner_quorum_sets: vec![],
};
let expected = bitsetvec![{0, 1}, {0, 2}, {1, 2}];
let actual = qset.to_blocking_sets();
assert_eq!(expected, actual);
}
#[test]
fn unsatisfiable_quorum_set_to_blocking_sets() {
let quorum_set = QuorumSet::new_unsatisfiable();
let expected = bitsetvec![{}];
let actual = quorum_set.to_blocking_sets();
assert_eq!(expected, actual);
}
#[test]
fn empty_quorum_set_to_blocking_sets() {
let quorum_set = QuorumSet::new_empty();
let expected: Vec<NodeIdSet> = bitsetvec![];
let actual = quorum_set.to_blocking_sets();
assert_eq!(expected, actual);
}
#[test]
fn minimal_blocking_sets_in_symmetric_consensus_cluster() {
let fbas = Fbas::from_json_str(
r#"[
{
"publicKey": "n0",
"quorumSet": { "threshold": 2, "validators": ["n0", "n1"] }
},
{
"publicKey": "n1",
"quorumSet": { "threshold": 2, "validators": ["n0", "n1"] }
}
]"#,
);
let expected = bitsetvec![{ 0 }, { 1 }];
let actual = find_minimal_blocking_sets(&fbas);
assert_eq!(expected, actual);
}
#[test]
fn minimal_blocking_sets_in_different_consensus_clusters() {
let fbas = Fbas::from_json_str(
r#"[
{
"publicKey": "n0",
"quorumSet": { "threshold": 1, "validators": ["n1"] }
},
{
"publicKey": "n1",
"quorumSet": { "threshold": 2, "validators": ["n0", "n1"] }
},
{
"publicKey": "n2",
"quorumSet": { "threshold": 2, "validators": ["n2", "n3"] }
},
{
"publicKey": "n3",
"quorumSet": { "threshold": 2, "validators": ["n2", "n3"] }
}
]"#,
);
let expected = vec![bitset![0, 2], bitset![0, 3], bitset![1, 2], bitset![1, 3]];
let actual = find_minimal_blocking_sets(&fbas);
assert_eq!(expected, actual);
}
#[test]
fn minimal_blocking_sets_in_different_symmetric_consensus_clusters() {
let fbas = Fbas::from_json_str(
r#"[
{
"publicKey": "n0",
"quorumSet": { "threshold": 2, "validators": ["n0", "n1"] }
},
{
"publicKey": "n1",
"quorumSet": { "threshold": 2, "validators": ["n0", "n1"] }
},
{
"publicKey": "n2",
"quorumSet": { "threshold": 2, "validators": ["n2", "n3"] }
},
{
"publicKey": "n3",
"quorumSet": { "threshold": 2, "validators": ["n2", "n3"] }
}
]"#,
);
let expected = vec![bitset![0, 2], bitset![0, 3], bitset![1, 2], bitset![1, 3]];
let actual = find_minimal_blocking_sets(&fbas);
assert_eq!(expected, actual);
}
#[test]
#[ignore]
fn minimal_blocking_sets_more_minimal_than_minimal_quorums() {
let fbas = Fbas::from_json_file(Path::new("test_data/stellarbeat_nodes_2019-09-17.json"));
let fbas = fbas.to_standard_form();
let minimal_quorums = find_minimal_quorums(&fbas);
let minimal_blocking_sets = find_minimal_blocking_sets(&fbas);
let minimal_all = remove_non_minimal_node_sets(
minimal_blocking_sets
.iter()
.chain(minimal_quorums.iter())
.cloned()
.collect(),
);
assert_eq!(minimal_blocking_sets, minimal_all);
}
}