use std::collections::{BTreeMap, BTreeSet};
use cyber_hemera::hash as hemera_hash;
use bbg::Particle;
use crate::chain::{CyberlinkRecord, Signal};
pub type ConflictKey = Particle;
pub fn conflict_keys(sig: &Signal) -> Vec<ConflictKey> {
vec![equivocation_key(sig)]
}
pub fn equivocation_key(sig: &Signal) -> ConflictKey {
let mut buf = [0u8; 40]; buf[..32].copy_from_slice(&sig.neuron);
buf[32..40].copy_from_slice(&sig.step.to_le_bytes());
let h = hemera_hash(&buf);
*h.as_bytes().first_chunk::<32>().unwrap_or(&[0u8; 32])
}
pub struct ConflictGroup {
pub key: ConflictKey,
members: BTreeMap<Particle, Signal>,
}
impl ConflictGroup {
pub fn members(&self) -> Vec<Signal> {
self.members.values().cloned().collect()
}
pub fn len(&self) -> usize {
self.members.len()
}
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
}
#[derive(Default)]
pub struct ConflictIndex {
groups: BTreeMap<ConflictKey, BTreeMap<Particle, Signal>>,
}
impl ConflictIndex {
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, sig: &Signal) -> Vec<ConflictKey> {
let id = sig.content_id();
let mut newly_conflicting = Vec::new();
for key in conflict_keys(sig) {
let group = self.groups.entry(key).or_default();
group.insert(id, sig.clone());
if group.len() > 1 {
newly_conflicting.push(key);
}
}
newly_conflicting
}
pub fn group(&self, key: &ConflictKey) -> Option<ConflictGroup> {
self.groups.get(key).map(|members| ConflictGroup {
key: *key,
members: members.clone(),
})
}
pub fn conflicts(&self) -> Vec<ConflictKey> {
self.groups
.iter()
.filter(|(_, m)| m.len() > 1)
.map(|(k, _)| *k)
.collect()
}
pub fn all_links(&self) -> Vec<CyberlinkRecord> {
let mut seen: BTreeSet<Particle> = BTreeSet::new();
let mut out = Vec::new();
for group in self.groups.values() {
for (id, sig) in group {
if seen.insert(*id) {
out.extend(sig.links.iter().cloned());
}
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chain::CyberlinkRecord;
fn link(from: u8, to: u8) -> CyberlinkRecord {
CyberlinkRecord {
neuron: [1u8; 32],
from: [from; 32],
to: [to; 32],
token: [0u8; 32],
amount: 1,
valence: 1,
height: 0,
}
}
fn sig(neuron: u8, step: u64, from: u8, to: u8) -> Signal {
Signal {
neuron: [neuron; 32],
network: SELF_NETWORK,
links: vec![link(from, to)],
delta_pi: vec![],
prev: [0u8; 32],
step,
height: 0,
proof: None,
}
}
use crate::chain::SELF_NETWORK;
#[test]
fn no_conflict_for_distinct_steps() {
let mut ix = ConflictIndex::new();
assert!(ix.observe(&sig(1, 0, 2, 3)).is_empty());
assert!(ix.observe(&sig(1, 1, 2, 3)).is_empty());
assert!(ix.conflicts().is_empty());
}
#[test]
fn equivocation_detected_same_neuron_same_step() {
let mut ix = ConflictIndex::new();
assert!(ix.observe(&sig(1, 0, 2, 3)).is_empty());
let conflicted = ix.observe(&sig(1, 0, 4, 5));
assert_eq!(conflicted.len(), 1);
let g = ix.group(&conflicted[0]).unwrap();
assert_eq!(g.len(), 2);
}
#[test]
fn reobserving_identical_signal_is_idempotent() {
let mut ix = ConflictIndex::new();
let s = sig(1, 0, 2, 3);
assert!(ix.observe(&s).is_empty());
assert!(ix.observe(&s).is_empty());
assert!(ix.conflicts().is_empty());
}
#[test]
fn detection_is_order_independent() {
let a = sig(1, 0, 2, 3);
let b = sig(1, 0, 4, 5);
let mut ix1 = ConflictIndex::new();
ix1.observe(&a);
ix1.observe(&b);
let mut ix2 = ConflictIndex::new();
ix2.observe(&b);
ix2.observe(&a);
assert_eq!(ix1.conflicts(), ix2.conflicts());
let k = ix1.conflicts()[0];
let m1: Vec<_> = ix1.group(&k).unwrap().members().iter().map(|s| s.content_id()).collect();
let m2: Vec<_> = ix2.group(&k).unwrap().members().iter().map(|s| s.content_id()).collect();
assert_eq!(m1, m2);
}
}