diff options
| author | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
|---|---|---|
| committer | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
| commit | 9f5d864d533ce86459e654f5d78212933c0269ea (patch) | |
| tree | 4b71e8c7ab4e78da93e5f3164803da4de68c7031 /network/src | |
| download | onioncoin-main.tar.gz onioncoin-main.tar.xz onioncoin-main.zip | |
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
Diffstat (limited to 'network/src')
| -rw-r--r-- | network/src/dandelion.rs | 270 | ||||
| -rw-r--r-- | network/src/lib.rs | 5 | ||||
| -rw-r--r-- | network/src/propagation.rs | 238 |
3 files changed, 513 insertions, 0 deletions
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<TxId> { + /// Current phase for each transaction + phases: HashMap<TxId, DandelionPhase>, + + /// Hop count for STEM phase + stem_hops: HashMap<TxId, u32>, + + /// 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<TxId: std::hash::Hash + Eq + Clone> DandelionRouter<TxId> { + 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<DandelionPhase> { + 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<DelayStrategy>, +} + +#[derive(Debug, Clone)] +pub enum TargetPeers { + /// Single peer (for STEM) + Single(PeerId), + + /// Multiple peers (for FLUFF broadcast) + Multiple(Vec<PeerId>), + + /// 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<PeerId>, delay: Option<DelayStrategy>) -> 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::<u64>::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::<u64>::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::<u64>::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::<u64>::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<Mutex<DandelionRouter<Vec<u8>>>>, + + /// Timing obfuscator + obfuscator: Arc<Mutex<TimingObfuscator<Transaction>>>, + + /// Connected peers + peers: Arc<Mutex<Vec<PeerId>>>, +} + +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<PropagationManager>, +} + +impl StandardPropagation { + pub fn new(manager: Arc<PropagationManager>) -> 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); + } +} |
