sb-mesh 0.1.1

S&B Sovereign Mesh (sb-mesh) — User-Space P2P Overlay Network, WireGuard-compatible Crypto & TUI
Documentation
use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::peer::PeerTable;

/// Types of Gossip Payload Disseminated Across the Swarm
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GossipKind {
    /// Certificate / Key Revocation List (CRL) update for compromised nodes
    RevokeNode {
        revoked_pubkey: String,
        reason: String,
        revoked_at: u64,
    },
    /// Swarm Zone Record synchronization across peers (CRDT state sync)
    ZoneRecordSync {
        domain: String,
        target_node_id: String,
        record_type: String,
        ttl: u64,
    },
    /// Lightweight anti-entropy ping
    PingGossip {
        nonce: u64,
    },
}

/// Cryptographically Identified Epidemic Gossip Message
#[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 {
    /// Computes unique content hash ID for deduplication and anti-entropy
    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())
    }
}

/// Outcome of processing an incoming epidemic gossip message
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GossipAction {
    /// Forward message to neighboring peers (Epidemic fanout)
    Forward(GossipMessage),
    /// Message already observed in history (Deduplicated)
    IgnoredAlreadySeen,
    /// Message successfully processed locally without further propagation
    AppliedLocally(String),
    /// Malformed or rejected
    RejectedInvalid(String),
}

/// Decentralized P2P Gossip & Anti-Entropy Engine
#[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(),
        }
    }

    /// Check if a given public key has been revoked by network consensus
    pub fn is_revoked(&self, pubkey_b64: &str) -> bool {
        self.revocation_list.contains(pubkey_b64)
    }

    /// Creates an authoritative node revocation announcement
    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)
    }

    /// Creates a swarm zone record announcement
    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())
    }

    /// Evaluates an incoming gossip message with anti-entropy deduplication
    pub fn process_incoming(
        &mut self,
        msg: GossipMessage,
        peer_table: Option<&mut PeerTable>,
    ) -> GossipAction {
        // 1. Anti-entropy deduplication
        if self.seen_messages.contains(&msg.id) {
            return GossipAction::IgnoredAlreadySeen;
        }

        // Verify ID integrity
        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());
        }

        // Mark as seen
        self.seen_messages.insert(msg.id);

        // 2. Apply state effect
        match &msg.kind {
            GossipKind::RevokeNode {
                revoked_pubkey,
                reason,
                revoked_at: _,
            } => {
                self.revocation_list.insert(revoked_pubkey.clone());
                if let Some(table) = peer_table {
                    // Evict compromised peer immediately
                    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 { .. } => {}
        }

        // 3. Return Forward action so node relays message to other peers
        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());

        // 1. Create Revocation
        let revocation_msg = GossipEngine::create_revocation(
            "sbm-0xauthority",
            evil_pubkey,
            "Device physically compromised",
            1,
            vec![1, 2, 3, 4],
        );

        // 2. Process first time -> Forward & Evict
        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");

        // 3. Process second time -> IgnoredAlreadySeen
        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")
        );
    }
}