use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Mutex;
use crate::types::{Prefix, Said, Threshold};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgreementStatus {
Pending {
collected: usize,
},
Accepted,
}
pub struct WitnessAgreement {
state: Mutex<AgreementState>,
}
struct AgreementState {
pending: HashMap<(String, u64, String), PendingEvent>,
eviction_order: VecDeque<(String, u64, String)>,
max_pending: usize,
accepted: HashSet<(String, u64, String)>,
}
struct PendingEvent {
witness_list: Vec<String>,
received: HashSet<String>,
threshold: Threshold,
}
impl PendingEvent {
fn is_satisfied(&self) -> bool {
let indices: Vec<u32> = self
.received
.iter()
.filter_map(|aid| self.witness_list.iter().position(|w| w == aid))
.map(|pos| pos as u32)
.collect();
self.threshold
.is_satisfied(&indices, self.witness_list.len())
}
}
impl WitnessAgreement {
pub fn new(max_pending: usize) -> Self {
Self {
state: Mutex::new(AgreementState {
pending: HashMap::new(),
eviction_order: VecDeque::new(),
max_pending,
accepted: HashSet::new(),
}),
}
}
pub fn submit_event(
&self,
prefix: &Prefix,
sn: u64,
said: &Said,
bt: &Threshold,
witnesses: &[Prefix],
) {
let key = (prefix.as_str().to_string(), sn, said.as_str().to_string());
let pending = PendingEvent {
witness_list: witnesses.iter().map(|w| w.as_str().to_string()).collect(),
received: HashSet::new(),
threshold: bt.clone(),
};
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
if state.accepted.contains(&key) {
return;
}
if pending.is_satisfied() {
state.accepted.insert(key);
return;
}
while state.pending.len() >= state.max_pending {
if let Some(evicted) = state.eviction_order.pop_front() {
state.pending.remove(&evicted);
} else {
break;
}
}
if !state.pending.contains_key(&key) {
state.eviction_order.push_back(key.clone());
state.pending.insert(key, pending);
}
}
pub fn add_receipt(
&self,
prefix: &Prefix,
sn: u64,
said: &Said,
witness_id: &str,
) -> AgreementStatus {
let key = (prefix.as_str().to_string(), sn, said.as_str().to_string());
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
if state.accepted.contains(&key) {
return AgreementStatus::Accepted;
}
if let Some(pending) = state.pending.get_mut(&key) {
pending.received.insert(witness_id.to_string());
let satisfied = pending.is_satisfied();
let collected = pending.received.len();
if satisfied {
state.pending.remove(&key);
state.accepted.insert(key);
return AgreementStatus::Accepted;
}
AgreementStatus::Pending { collected }
} else {
AgreementStatus::Pending { collected: 0 }
}
}
pub fn is_accepted(&self, prefix: &Prefix, sn: u64, said: &Said) -> bool {
let key = (prefix.as_str().to_string(), sn, said.as_str().to_string());
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
state.accepted.contains(&key)
}
pub fn status(&self, prefix: &Prefix, sn: u64, said: &Said) -> AgreementStatus {
let key = (prefix.as_str().to_string(), sn, said.as_str().to_string());
let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
if state.accepted.contains(&key) {
AgreementStatus::Accepted
} else if let Some(pending) = state.pending.get(&key) {
AgreementStatus::Pending {
collected: pending.received.len(),
}
} else {
AgreementStatus::Pending { collected: 0 }
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn backers(n: usize) -> Vec<Prefix> {
(1..=n)
.map(|i| Prefix::new_unchecked(format!("witness{i}")))
.collect()
}
#[test]
fn event_accepted_after_threshold_receipts() {
let agreement = WitnessAgreement::new(100);
let prefix = Prefix::new_unchecked("ETest".into());
let said = Said::new_unchecked("ESAID".into());
let bt = Threshold::Simple(2);
agreement.submit_event(&prefix, 0, &said, &bt, &backers(3));
let s1 = agreement.add_receipt(&prefix, 0, &said, "witness1");
assert_eq!(s1, AgreementStatus::Pending { collected: 1 });
let s2 = agreement.add_receipt(&prefix, 0, &said, "witness2");
assert_eq!(s2, AgreementStatus::Accepted);
assert!(agreement.is_accepted(&prefix, 0, &said));
}
#[test]
fn event_stays_pending_with_insufficient_receipts() {
let agreement = WitnessAgreement::new(100);
let prefix = Prefix::new_unchecked("ETest".into());
let said = Said::new_unchecked("ESAID".into());
let bt = Threshold::Simple(3);
agreement.submit_event(&prefix, 0, &said, &bt, &backers(5));
agreement.add_receipt(&prefix, 0, &said, "witness1");
agreement.add_receipt(&prefix, 0, &said, "witness2");
assert!(!agreement.is_accepted(&prefix, 0, &said));
}
#[test]
fn zero_threshold_immediately_accepted() {
let agreement = WitnessAgreement::new(100);
let prefix = Prefix::new_unchecked("ETest".into());
let said = Said::new_unchecked("ESAID".into());
let bt = Threshold::Simple(0);
agreement.submit_event(&prefix, 0, &said, &bt, &backers(0));
assert!(agreement.is_accepted(&prefix, 0, &said));
}
#[test]
fn duplicate_receipt_from_same_witness_not_double_counted() {
let agreement = WitnessAgreement::new(100);
let prefix = Prefix::new_unchecked("ETest".into());
let said = Said::new_unchecked("ESAID".into());
let bt = Threshold::Simple(2);
agreement.submit_event(&prefix, 0, &said, &bt, &backers(3));
agreement.add_receipt(&prefix, 0, &said, "witness1");
agreement.add_receipt(&prefix, 0, &said, "witness1");
assert!(!agreement.is_accepted(&prefix, 0, &said));
}
#[test]
fn fifo_eviction_at_capacity() {
let agreement = WitnessAgreement::new(2);
let prefix = Prefix::new_unchecked("ETest".into());
let bt = Threshold::Simple(2);
for i in 0..3 {
let said = Said::new_unchecked(format!("ESAID{i}"));
agreement.submit_event(&prefix, i as u64, &said, &bt, &backers(3));
}
let said0 = Said::new_unchecked("ESAID0".into());
let status = agreement.status(&prefix, 0, &said0);
assert_eq!(status, AgreementStatus::Pending { collected: 0 });
}
#[test]
fn weighted_bt_requires_quorum() {
use crate::types::Fraction;
let half = || Fraction {
numerator: 1,
denominator: 2,
};
let bt = Threshold::Weighted(vec![vec![half(), half(), half()]]);
let agreement = WitnessAgreement::new(100);
let prefix = Prefix::new_unchecked("ETest".into());
let said = Said::new_unchecked("ESAID".into());
agreement.submit_event(&prefix, 0, &said, &bt, &backers(3));
assert!(
!agreement.is_accepted(&prefix, 0, &said),
"zero receipts must not satisfy a weighted bt"
);
agreement.add_receipt(&prefix, 0, &said, "witness1");
assert!(!agreement.is_accepted(&prefix, 0, &said));
agreement.add_receipt(&prefix, 0, &said, "witness2");
assert!(agreement.is_accepted(&prefix, 0, &said));
}
}