use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::peer::PeerTable;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GossipKind {
RevokeNode {
revoked_pubkey: String,
reason: String,
revoked_at: u64,
},
ZoneRecordSync {
domain: String,
target_node_id: String,
record_type: String,
ttl: u64,
},
PingGossip {
nonce: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GossipMessage {
pub id: [u8; 32],
pub origin_node: String,
pub epoch: u64,
pub kind: GossipKind,
pub signature: Vec<u8>,
}
impl GossipMessage {
pub fn compute_id(origin_node: &str, epoch: u64, kind: &GossipKind) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"SBM_GOSSIP_V1:");
hasher.update(origin_node.as_bytes());
hasher.update(&epoch.to_le_bytes());
let kind_bytes = serde_json::to_vec(kind).unwrap_or_default();
hasher.update(&kind_bytes);
hasher.finalize().into()
}
pub fn new(origin_node: &str, epoch: u64, kind: GossipKind, signature: Vec<u8>) -> Self {
let id = Self::compute_id(origin_node, epoch, &kind);
Self {
id,
origin_node: origin_node.to_string(),
epoch,
kind,
signature,
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
serde_json::to_vec(self).map_err(|e| e.to_string())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
serde_json::from_slice(bytes).map_err(|e| e.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GossipAction {
Forward(GossipMessage),
IgnoredAlreadySeen,
AppliedLocally(String),
RejectedInvalid(String),
}
#[derive(Debug, Clone, Default)]
pub struct GossipEngine {
pub seen_messages: HashSet<[u8; 32]>,
pub revocation_list: HashSet<String>,
pub replicated_zones: HashMap<String, String>,
}
impl GossipEngine {
pub fn new() -> Self {
Self {
seen_messages: HashSet::new(),
revocation_list: HashSet::new(),
replicated_zones: HashMap::new(),
}
}
pub fn is_revoked(&self, pubkey_b64: &str) -> bool {
self.revocation_list.contains(pubkey_b64)
}
pub fn create_revocation(
origin_node: &str,
revoked_pubkey: &str,
reason: &str,
epoch: u64,
dummy_sig: Vec<u8>,
) -> GossipMessage {
let now = chrono::Utc::now().timestamp() as u64;
let kind = GossipKind::RevokeNode {
revoked_pubkey: revoked_pubkey.to_string(),
reason: reason.to_string(),
revoked_at: now,
};
GossipMessage::new(origin_node, epoch, kind, dummy_sig)
}
pub fn create_zone_sync(
origin_node: &str,
domain: &str,
target_node_id: &str,
epoch: u64,
) -> GossipMessage {
let kind = GossipKind::ZoneRecordSync {
domain: domain.to_string(),
target_node_id: target_node_id.to_string(),
record_type: "A/OVERLAY".to_string(),
ttl: 300,
};
GossipMessage::new(origin_node, epoch, kind, Vec::new())
}
pub fn process_incoming(
&mut self,
msg: GossipMessage,
peer_table: Option<&mut PeerTable>,
) -> GossipAction {
if self.seen_messages.contains(&msg.id) {
return GossipAction::IgnoredAlreadySeen;
}
let expected_id = GossipMessage::compute_id(&msg.origin_node, msg.epoch, &msg.kind);
if msg.id != expected_id {
return GossipAction::RejectedInvalid("Tampered gossip message ID".to_string());
}
self.seen_messages.insert(msg.id);
match &msg.kind {
GossipKind::RevokeNode {
revoked_pubkey,
reason,
revoked_at: _,
} => {
self.revocation_list.insert(revoked_pubkey.clone());
if let Some(table) = peer_table {
table.remove_peer(revoked_pubkey);
}
println!(
"[GOSSIP CRL] Revoked key '{}' evicted (Reason: {})",
revoked_pubkey, reason
);
}
GossipKind::ZoneRecordSync {
domain,
target_node_id,
..
} => {
self.replicated_zones
.insert(domain.clone(), target_node_id.clone());
}
GossipKind::PingGossip { .. } => {}
}
GossipAction::Forward(msg)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer::PeerConfig;
use chrono::Utc;
#[test]
fn test_gossip_deduplication_and_crl_eviction() {
let mut engine = GossipEngine::new();
let mut table = PeerTable::new();
let evil_pubkey = "ZXhwbG9pdGVkX2tleV9mb3JfcmV2b2NhdGlvbg==";
table.add_peer(PeerConfig {
callsign: "compromised-node".to_string(),
node_id: "sbm-0xevil".to_string(),
public_key_base64: evil_pubkey.to_string(),
endpoint: Some("192.168.1.150:58888".to_string()),
overlay_ip: None,
created_at: Utc::now(),
});
assert!(table.get_by_pubkey(evil_pubkey).is_some());
let revocation_msg = GossipEngine::create_revocation(
"sbm-0xauthority",
evil_pubkey,
"Device physically compromised",
1,
vec![1, 2, 3, 4],
);
let action = engine.process_incoming(revocation_msg.clone(), Some(&mut table));
match action {
GossipAction::Forward(m) => assert_eq!(m.id, revocation_msg.id),
other => panic!("Expected Forward action, got {:?}", other),
}
assert!(engine.is_revoked(evil_pubkey));
assert!(table.get_by_pubkey(evil_pubkey).is_none(), "Compromised peer must be evicted from peer table");
let action2 = engine.process_incoming(revocation_msg, Some(&mut table));
assert_eq!(action2, GossipAction::IgnoredAlreadySeen);
}
#[test]
fn test_gossip_zone_record_sync() {
let mut engine = GossipEngine::new();
let zone_msg = GossipEngine::create_zone_sync(
"sbm-0xalpha",
"vault.sbm",
"sbm-0xvault_node",
1,
);
let action = engine.process_incoming(zone_msg, None);
assert!(matches!(action, GossipAction::Forward(_)));
assert_eq!(
engine.replicated_zones.get("vault.sbm").map(|s| s.as_str()),
Some("sbm-0xvault_node")
);
}
}