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/Cargo.toml | 18 +++ network/src/dandelion.rs | 270 +++++++++++++++++++++++++++++++++++++++++++++ network/src/lib.rs | 5 + network/src/propagation.rs | 238 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 531 insertions(+) create mode 100644 network/Cargo.toml create mode 100644 network/src/dandelion.rs create mode 100644 network/src/lib.rs create mode 100644 network/src/propagation.rs (limited to 'network') diff --git a/network/Cargo.toml b/network/Cargo.toml new file mode 100644 index 0000000..679600a --- /dev/null +++ b/network/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "onioncoin-network" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +onioncoin-timing = { path = "../timing" } +onioncoin-core = { path = "../core" } + +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +async-trait.workspace = true +rand.workspace = true +thiserror.workspace = true +chrono.workspace = true 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); + } +} diff --git a/network/src/lib.rs b/network/src/lib.rs new file mode 100644 index 0000000..36d98c6 --- /dev/null +++ b/network/src/lib.rs @@ -0,0 +1,5 @@ +pub mod dandelion; +pub mod propagation; + +pub use dandelion::{DandelionRouter, DandelionPhase}; +pub use propagation::{PropagationManager, PropagationStrategy}; diff --git a/network/src/propagation.rs b/network/src/propagation.rs new file mode 100644 index 0000000..519f5f4 --- /dev/null +++ b/network/src/propagation.rs @@ -0,0 +1,238 @@ +use crate::dandelion::{DandelionRouter, DandelionPhase, RoutingDecision, PeerId}; +use onioncoin_timing::{TimingObfuscator, obfuscation::ObfuscationConfig, DelayStrategy}; +use onioncoin_core::Transaction; +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Manages transaction propagation with timing obfuscation +pub struct PropagationManager { + /// Dandelion++ router + dandelion: Arc>>>, + + /// Timing obfuscator + obfuscator: Arc>>, + + /// Connected peers + peers: Arc>>, +} + +impl PropagationManager { + pub fn new() -> Self { + let config = ObfuscationConfig::default(); + + Self { + dandelion: Arc::new(Mutex::new(DandelionRouter::new(Default::default()))), + obfuscator: Arc::new(Mutex::new(TimingObfuscator::new(config))), + peers: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Receive a new transaction from wallet/user + pub async fn receive_transaction(&self, tx: Transaction) -> PropagationResult { + let tx_id = tx.id(); + + // Register with Dandelion router + { + let mut dandelion = self.dandelion.lock().await; + dandelion.register_transaction(tx_id.to_vec()); + } + + // Add to mixing pool + { + let mut obfuscator = self.obfuscator.lock().await; + obfuscator.add_to_pool(tx); + } + + PropagationResult::Queued + } + + /// Process pending transactions for propagation + pub async fn process_pending(&self) -> Vec<(Transaction, RoutingDecision)> { + let mut results = Vec::new(); + + // Check if should release batch from mixing pool + let should_release = { + let obfuscator = self.obfuscator.lock().await; + obfuscator.should_release_batch() + }; + + if !should_release { + return results; + } + + // Release batch + let batch = { + let mut obfuscator = self.obfuscator.lock().await; + obfuscator.release_batch() + }; + + // Route each transaction + for tx in batch { + let tx_id = tx.id(); + let routing = self.route_transaction(&tx_id).await; + results.push((tx, routing)); + } + + results + } + + /// Route a single transaction + async fn route_transaction(&self, tx_id: &[u8]) -> RoutingDecision { + let mut dandelion = self.dandelion.lock().await; + let phase = dandelion.process_hop(&tx_id.to_vec()); + + match phase { + DandelionPhase::Stem => { + // Select random peer for STEM + let peer = self.select_random_peer().await; + let delay = DelayStrategy::dandelion_stem(); + RoutingDecision::stem(peer, delay) + } + + DandelionPhase::Fluff => { + // Broadcast to random subset of peers + let peers = self.peers.lock().await; + let subset_size = (peers.len() as f64).sqrt() as usize; + RoutingDecision::fluff_random(subset_size.max(1)) + } + } + } + + /// Select random peer for STEM routing + async fn select_random_peer(&self) -> PeerId { + use rand::seq::SliceRandom; + + let peers = self.peers.lock().await; + peers + .choose(&mut rand::thread_rng()) + .cloned() + .unwrap_or_else(|| "default".to_string()) + } + + /// Add peer to connection pool + pub async fn add_peer(&self, peer: PeerId) { + let mut peers = self.peers.lock().await; + if !peers.contains(&peer) { + peers.push(peer); + } + } + + /// Remove peer from connection pool + pub async fn remove_peer(&self, peer: &PeerId) { + let mut peers = self.peers.lock().await; + peers.retain(|p| p != peer); + } + + /// Get number of connected peers + pub async fn peer_count(&self) -> usize { + self.peers.lock().await.len() + } +} + +impl Default for PropagationManager { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PropagationResult { + /// Transaction queued for propagation + Queued, + + /// Transaction propagated immediately + Propagated, + + /// Transaction rejected + Rejected, +} + +/// Strategy for propagating transactions +#[async_trait] +pub trait PropagationStrategy: Send + Sync { + /// Decide how to propagate a transaction + async fn route(&self, tx: &Transaction) -> RoutingDecision; + + /// Apply timing delays + async fn apply_delay(&self, delay: &DelayStrategy); +} + +/// Standard propagation strategy (Dandelion++ with timing obfuscation) +pub struct StandardPropagation { + manager: Arc, +} + +impl StandardPropagation { + pub fn new(manager: Arc) -> Self { + Self { manager } + } +} + +#[async_trait] +impl PropagationStrategy for StandardPropagation { + async fn route(&self, tx: &Transaction) -> RoutingDecision { + let tx_id = tx.id(); + self.manager.route_transaction(&tx_id).await + } + + async fn apply_delay(&self, delay: &DelayStrategy) { + delay.sleep().await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use onioncoin_timing::TimingMetadata; + use chrono::Utc; + + fn create_test_tx() -> Transaction { + use onioncoin_core::transaction::*; + + let seed = [42u8; 32]; + let timing = TimingMetadata::new(Utc::now(), &seed).unwrap(); + + Transaction { + version: 1, + inputs: vec![TransactionInput { + previous_output: OutPoint { + txid: [1u8; 32], + vout: 0, + }, + key_image: [2u8; 32], + }], + outputs: vec![TransactionOutput { + encrypted_amount: vec![0u8; 32], + stealth_address: [3u8; 32], + commitment: [4u8; 32], + }], + ring_signatures: vec![RingSignature::new(11)], + range_proof: vec![0u8; 64], + encrypted_fee: 1000, + timing, + } + } + + #[tokio::test] + async fn test_propagation_manager() { + let manager = PropagationManager::new(); + let tx = create_test_tx(); + + let result = manager.receive_transaction(tx).await; + assert_eq!(result, PropagationResult::Queued); + } + + #[tokio::test] + async fn test_peer_management() { + let manager = PropagationManager::new(); + + manager.add_peer("peer1".to_string()).await; + manager.add_peer("peer2".to_string()).await; + + assert_eq!(manager.peer_count().await, 2); + + manager.remove_peer(&"peer1".to_string()).await; + assert_eq!(manager.peer_count().await, 1); + } +} -- cgit v1.2.3