summaryrefslogtreecommitdiffstats
path: root/network
diff options
context:
space:
mode:
Diffstat (limited to 'network')
-rw-r--r--network/Cargo.toml18
-rw-r--r--network/src/dandelion.rs270
-rw-r--r--network/src/lib.rs5
-rw-r--r--network/src/propagation.rs238
4 files changed, 531 insertions, 0 deletions
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<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);
+ }
+}