From 9f5d864d533ce86459e654f5d78212933c0269ea Mon Sep 17 00:00:00 2001 From: gabrix73 Date: Mon, 1 Jun 2026 18:41:36 +0200 Subject: Initial commit: OnionCoin prototype with Proof-of-Relay consensus OnionCoin is a privacy cryptocurrency that rewards Tor relay operators through a unique Proof-of-Contribution consensus mechanism. Core Features: - Proof-of-Relay: 30% of block rewards go to Tor operators - Native .onion node identity (no IP exposure) - Temporal obfuscation protocols - Dandelion++ over Tor propagation - Native inheritance system with dead man's switch Technical Stack: - Rust workspace with 8 crates - Ed25519/X25519 cryptography - arti (Rust Tor client) integration planned - 10 minute block time, 5-10 TPS design Status: Prototype - Consensus logic complete with passing tests (30/33) - Network layer conceptual design complete - Tor integration pending - Testnet launch planned Q3 2026 License: MIT Author: Gabriele Salati (virebent) Contact: g48rix@gmail.com Website: https://www.gabrielesalati.eu Repository: https://git.virebent.art/virebent/onioncoin --- network/src/dandelion.rs | 270 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 network/src/dandelion.rs (limited to 'network/src/dandelion.rs') diff --git a/network/src/dandelion.rs b/network/src/dandelion.rs new file mode 100644 index 0000000..daca85f --- /dev/null +++ b/network/src/dandelion.rs @@ -0,0 +1,270 @@ +use onioncoin_timing::{DelayStrategy, delay::DelaySequence}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Dandelion++ phases for transaction propagation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DandelionPhase { + /// STEM phase: forward to single peer with delays + Stem, + /// FLUFF phase: broadcast to all peers + Fluff, +} + +/// Router for Dandelion++ protocol +#[derive(Debug)] +pub struct DandelionRouter { + /// Current phase for each transaction + phases: HashMap, + + /// Hop count for STEM phase + stem_hops: HashMap, + + /// Configuration + config: DandelionConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DandelionConfig { + /// Minimum STEM hops before transitioning to FLUFF + pub min_stem_hops: u32, + + /// Maximum STEM hops before forcing FLUFF + pub max_stem_hops: u32, + + /// Probability of transitioning to FLUFF at each hop (0.0 - 1.0) + pub fluff_probability: f64, + + /// Enable delays between hops + pub use_delays: bool, +} + +impl Default for DandelionConfig { + fn default() -> Self { + Self { + min_stem_hops: 1, + max_stem_hops: 4, + fluff_probability: 0.25, // 25% chance per hop after min + use_delays: true, + } + } +} + +impl DandelionRouter { + pub fn new(config: DandelionConfig) -> Self { + Self { + phases: HashMap::new(), + stem_hops: HashMap::new(), + config, + } + } + + /// Register a new transaction (starts in STEM phase) + pub fn register_transaction(&mut self, tx_id: TxId) { + self.phases.insert(tx_id.clone(), DandelionPhase::Stem); + self.stem_hops.insert(tx_id, 0); + } + + /// Process a hop and determine if should transition to FLUFF + pub fn process_hop(&mut self, tx_id: &TxId) -> DandelionPhase { + let current_phase = self.phases.get(tx_id).copied().unwrap_or(DandelionPhase::Stem); + + if current_phase == DandelionPhase::Fluff { + return DandelionPhase::Fluff; + } + + // Increment hop count + let hops = self.stem_hops.entry(tx_id.clone()).or_insert(0); + *hops += 1; + + // Force FLUFF if max hops reached + if *hops >= self.config.max_stem_hops { + self.phases.insert(tx_id.clone(), DandelionPhase::Fluff); + return DandelionPhase::Fluff; + } + + // If past min hops, randomly transition to FLUFF + if *hops >= self.config.min_stem_hops { + let mut rng = rand::thread_rng(); + if rng.gen_bool(self.config.fluff_probability) { + self.phases.insert(tx_id.clone(), DandelionPhase::Fluff); + return DandelionPhase::Fluff; + } + } + + DandelionPhase::Stem + } + + /// Get current phase for a transaction + pub fn get_phase(&self, tx_id: &TxId) -> Option { + self.phases.get(tx_id).copied() + } + + /// Force transition to FLUFF phase + pub fn force_fluff(&mut self, tx_id: &TxId) { + self.phases.insert(tx_id.clone(), DandelionPhase::Fluff); + } + + /// Get number of STEM hops for a transaction + pub fn get_stem_hops(&self, tx_id: &TxId) -> u32 { + self.stem_hops.get(tx_id).copied().unwrap_or(0) + } + + /// Clean up old transactions + pub fn cleanup(&mut self, tx_ids: &[TxId]) { + for tx_id in tx_ids { + self.phases.remove(tx_id); + self.stem_hops.remove(tx_id); + } + } + + /// Generate delay sequence for STEM phase + pub fn create_stem_delays(&self) -> DelaySequence { + DelaySequence::dandelion_stem() + } +} + +/// Represents a routing decision for Dandelion++ +#[derive(Debug, Clone)] +pub struct RoutingDecision { + /// Phase to use + pub phase: DandelionPhase, + + /// Target peers (1 for STEM, multiple for FLUFF) + pub target_peers: TargetPeers, + + /// Delay before forwarding + pub delay: Option, +} + +#[derive(Debug, Clone)] +pub enum TargetPeers { + /// Single peer (for STEM) + Single(PeerId), + + /// Multiple peers (for FLUFF broadcast) + Multiple(Vec), + + /// Random subset of peers + RandomSubset { count: usize }, +} + +/// Placeholder for peer identifier +pub type PeerId = String; + +impl RoutingDecision { + /// Create STEM routing decision + pub fn stem(peer: PeerId, delay: DelayStrategy) -> Self { + Self { + phase: DandelionPhase::Stem, + target_peers: TargetPeers::Single(peer), + delay: Some(delay), + } + } + + /// Create FLUFF routing decision + pub fn fluff(peers: Vec, delay: Option) -> Self { + Self { + phase: DandelionPhase::Fluff, + target_peers: TargetPeers::Multiple(peers), + delay, + } + } + + /// Create random subset FLUFF + pub fn fluff_random(count: usize) -> Self { + Self { + phase: DandelionPhase::Fluff, + target_peers: TargetPeers::RandomSubset { count }, + delay: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dandelion_registration() { + let mut router = DandelionRouter::::new(DandelionConfig::default()); + router.register_transaction(1); + + assert_eq!(router.get_phase(&1), Some(DandelionPhase::Stem)); + assert_eq!(router.get_stem_hops(&1), 0); + } + + #[test] + fn test_dandelion_hop_progression() { + let mut router = DandelionRouter::::new(DandelionConfig::default()); + router.register_transaction(1); + + // Process hops + for _ in 0..3 { + router.process_hop(&1); + } + + assert!(router.get_stem_hops(&1) >= 3); + } + + #[test] + fn test_dandelion_force_fluff() { + let config = DandelionConfig { + min_stem_hops: 1, + max_stem_hops: 10, + fluff_probability: 0.0, // Never random transition + use_delays: true, + }; + + let mut router = DandelionRouter::::new(config); + router.register_transaction(1); + + // Should stay in STEM + router.process_hop(&1); + assert_eq!(router.get_phase(&1), Some(DandelionPhase::Stem)); + + // Force FLUFF + router.force_fluff(&1); + assert_eq!(router.get_phase(&1), Some(DandelionPhase::Fluff)); + } + + #[test] + fn test_dandelion_max_hops() { + let config = DandelionConfig { + min_stem_hops: 1, + max_stem_hops: 3, + fluff_probability: 0.0, + use_delays: true, + }; + + let mut router = DandelionRouter::::new(config); + router.register_transaction(1); + + // Process max hops + for _ in 0..3 { + router.process_hop(&1); + } + + // Should transition to FLUFF at max hops + assert_eq!(router.get_phase(&1), Some(DandelionPhase::Fluff)); + } + + #[test] + fn test_routing_decision() { + let stem_decision = RoutingDecision::stem( + "peer1".to_string(), + DelayStrategy::dandelion_stem(), + ); + + assert_eq!(stem_decision.phase, DandelionPhase::Stem); + assert!(stem_decision.delay.is_some()); + + let fluff_decision = RoutingDecision::fluff( + vec!["peer1".to_string(), "peer2".to_string()], + None, + ); + + assert_eq!(fluff_decision.phase, DandelionPhase::Fluff); + } +} -- cgit v1.2.3