kaccy_bitcoin/
chain_privacy.rs

1//! Transaction graph obfuscation for chain analysis resistance
2//!
3//! Provides strategies to resist blockchain analysis by obfuscating transaction
4//! patterns and timing.
5
6use crate::btc_utils::round_for_privacy;
7use crate::error::BitcoinError;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Transaction structure randomization options
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct StructureRandomization {
14    /// Add decoy outputs
15    pub add_decoy_outputs: bool,
16    /// Randomize output order
17    pub randomize_output_order: bool,
18    /// Randomize input order
19    pub randomize_input_order: bool,
20    /// Add random nSequence values
21    pub randomize_nsequence: bool,
22}
23
24impl Default for StructureRandomization {
25    fn default() -> Self {
26        Self {
27            add_decoy_outputs: true,
28            randomize_output_order: true,
29            randomize_input_order: true,
30            randomize_nsequence: false,
31        }
32    }
33}
34
35/// Amount obfuscation strategy
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum AmountObfuscation {
38    /// No obfuscation
39    None,
40    /// Round to nearest power of 10
41    RoundPowerOfTen,
42    /// Round to specific denomination
43    RoundDenomination { sats: u64 },
44    /// Add random dust to amounts
45    AddRandomDust { max_dust: u64 },
46}
47
48/// Timing analysis resistance configuration
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TimingObfuscation {
51    /// Add random delay before broadcasting (seconds)
52    pub random_delay_secs: Option<(u64, u64)>, // (min, max)
53    /// Broadcast at specific time of day (UTC hour)
54    pub broadcast_hour: Option<u8>,
55    /// Batch with other transactions
56    pub batch_broadcast: bool,
57}
58
59impl Default for TimingObfuscation {
60    fn default() -> Self {
61        Self {
62            random_delay_secs: Some((0, 300)), // 0-5 minutes
63            broadcast_hour: None,
64            batch_broadcast: false,
65        }
66    }
67}
68
69/// Transaction privacy enhancer
70pub struct TransactionPrivacyEnhancer {
71    structure_randomization: StructureRandomization,
72    amount_obfuscation: AmountObfuscation,
73    timing_obfuscation: TimingObfuscation,
74}
75
76impl TransactionPrivacyEnhancer {
77    /// Create a new privacy enhancer
78    pub fn new(
79        structure_randomization: StructureRandomization,
80        amount_obfuscation: AmountObfuscation,
81        timing_obfuscation: TimingObfuscation,
82    ) -> Self {
83        Self {
84            structure_randomization,
85            amount_obfuscation,
86            timing_obfuscation,
87        }
88    }
89
90    /// Apply amount obfuscation
91    pub fn obfuscate_amount(&self, amount: u64) -> u64 {
92        match &self.amount_obfuscation {
93            AmountObfuscation::None => amount,
94            AmountObfuscation::RoundPowerOfTen => round_for_privacy(amount, 10_000),
95            AmountObfuscation::RoundDenomination { sats } => {
96                let remainder = amount % sats;
97                if remainder < sats / 2 {
98                    amount - remainder
99                } else {
100                    amount + (sats - remainder)
101                }
102            }
103            AmountObfuscation::AddRandomDust { max_dust } => {
104                use rand::Rng;
105                let mut rng = rand::rng();
106                let dust = rng.random_range(0..*max_dust);
107                amount + dust
108            }
109        }
110    }
111
112    /// Generate decoy output amount
113    pub fn generate_decoy_amount(&self, total_amount: u64) -> u64 {
114        use rand::Rng;
115        let mut rng = rand::rng();
116        // Decoy should be between 1% and 20% of total
117        let min = total_amount / 100;
118        let max = total_amount / 5;
119        if min >= max {
120            return min;
121        }
122        rng.random_range(min..=max)
123    }
124
125    /// Check if decoy output should be added
126    pub fn should_add_decoy(&self) -> bool {
127        use rand::Rng;
128        self.structure_randomization.add_decoy_outputs && {
129            let mut rng = rand::rng();
130            rng.random_bool(0.3) // 30% chance
131        }
132    }
133
134    /// Calculate broadcast delay in seconds
135    pub fn calculate_broadcast_delay(&self) -> u64 {
136        use rand::Rng;
137        if let Some((min, max)) = self.timing_obfuscation.random_delay_secs {
138            let mut rng = rand::rng();
139            rng.random_range(min..=max)
140        } else {
141            0
142        }
143    }
144
145    /// Check if transaction should be batched
146    pub fn should_batch(&self) -> bool {
147        self.timing_obfuscation.batch_broadcast
148    }
149}
150
151impl Default for TransactionPrivacyEnhancer {
152    fn default() -> Self {
153        Self::new(
154            StructureRandomization::default(),
155            AmountObfuscation::RoundPowerOfTen,
156            TimingObfuscation::default(),
157        )
158    }
159}
160
161/// Change output strategy to resist fingerprinting
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum ChangeStrategy {
164    /// Standard change output
165    Standard,
166    /// Multiple change outputs to obfuscate
167    Multiple { count: usize },
168    /// Match payment output amount
169    MatchPayment,
170    /// Random amount split
171    RandomSplit,
172}
173
174/// Change output generator
175pub struct ChangeOutputGenerator {
176    strategy: ChangeStrategy,
177    min_change: u64,
178}
179
180impl ChangeOutputGenerator {
181    /// Create a new change output generator
182    pub fn new(strategy: ChangeStrategy, min_change: u64) -> Self {
183        Self {
184            strategy,
185            min_change,
186        }
187    }
188
189    /// Generate change outputs
190    pub fn generate_change_outputs(
191        &self,
192        total_change: u64,
193        payment_amount: Option<u64>,
194    ) -> Result<Vec<u64>, BitcoinError> {
195        if total_change < self.min_change {
196            return Ok(Vec::new());
197        }
198
199        match &self.strategy {
200            ChangeStrategy::Standard => Ok(vec![total_change]),
201            ChangeStrategy::Multiple { count } => self.split_change_multiple(total_change, *count),
202            ChangeStrategy::MatchPayment => {
203                if let Some(payment) = payment_amount {
204                    Ok(vec![payment, total_change.saturating_sub(payment)]).map(|outputs| {
205                        if outputs[1] < self.min_change {
206                            vec![total_change]
207                        } else {
208                            outputs
209                        }
210                    })
211                } else {
212                    Ok(vec![total_change])
213                }
214            }
215            ChangeStrategy::RandomSplit => self.split_change_random(total_change),
216        }
217    }
218
219    /// Split change into multiple outputs
220    fn split_change_multiple(&self, total: u64, count: usize) -> Result<Vec<u64>, BitcoinError> {
221        if count == 0 {
222            return Err(BitcoinError::InvalidTransaction(
223                "Count must be greater than 0".to_string(),
224            ));
225        }
226
227        if count == 1 {
228            return Ok(vec![total]);
229        }
230
231        let min_per_output = self.min_change;
232        if total < min_per_output * count as u64 {
233            return Ok(vec![total]);
234        }
235
236        use rand::Rng;
237        let mut outputs = Vec::new();
238        let mut remaining = total;
239        let mut rng = rand::rng();
240
241        for i in 0..count {
242            if i == count - 1 {
243                // Last output gets remainder
244                outputs.push(remaining);
245            } else {
246                let max_amount = remaining - (min_per_output * (count - i - 1) as u64);
247                let amount = rng.random_range(min_per_output..=max_amount);
248                outputs.push(amount);
249                remaining -= amount;
250            }
251        }
252
253        Ok(outputs)
254    }
255
256    /// Split change randomly
257    fn split_change_random(&self, total: u64) -> Result<Vec<u64>, BitcoinError> {
258        use rand::Rng;
259        let mut rng = rand::rng();
260        let count = rng.random_range(1..=3);
261        self.split_change_multiple(total, count)
262    }
263}
264
265/// Transaction timing coordinator
266pub struct TimingCoordinator {
267    /// Pending transactions awaiting broadcast
268    pending: HashMap<String, PendingBroadcast>,
269}
270
271/// Pending broadcast information
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct PendingBroadcast {
274    /// Transaction hex
275    pub tx_hex: String,
276    /// Scheduled broadcast time
277    pub broadcast_at: chrono::DateTime<chrono::Utc>,
278    /// Priority level
279    pub priority: BroadcastPriority,
280}
281
282/// Broadcast priority
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284pub enum BroadcastPriority {
285    /// Low priority - can be delayed
286    Low,
287    /// Normal priority
288    Normal,
289    /// High priority - broadcast soon
290    High,
291}
292
293impl TimingCoordinator {
294    /// Create a new timing coordinator
295    pub fn new() -> Self {
296        Self {
297            pending: HashMap::new(),
298        }
299    }
300
301    /// Schedule a transaction for broadcast
302    pub fn schedule_broadcast(
303        &mut self,
304        tx_id: String,
305        tx_hex: String,
306        delay_secs: u64,
307        priority: BroadcastPriority,
308    ) {
309        let broadcast_at = chrono::Utc::now() + chrono::Duration::seconds(delay_secs as i64);
310
311        self.pending.insert(
312            tx_id,
313            PendingBroadcast {
314                tx_hex,
315                broadcast_at,
316                priority,
317            },
318        );
319    }
320
321    /// Get transactions ready for broadcast
322    pub fn get_ready_broadcasts(&mut self) -> Vec<(String, String)> {
323        let now = chrono::Utc::now();
324        let mut ready = Vec::new();
325
326        let ready_ids: Vec<String> = self
327            .pending
328            .iter()
329            .filter(|(_, pending)| pending.broadcast_at <= now)
330            .map(|(id, _)| id.clone())
331            .collect();
332
333        for id in ready_ids {
334            if let Some(pending) = self.pending.remove(&id) {
335                ready.push((id, pending.tx_hex));
336            }
337        }
338
339        ready
340    }
341
342    /// Cancel a scheduled broadcast
343    pub fn cancel_broadcast(&mut self, tx_id: &str) -> bool {
344        self.pending.remove(tx_id).is_some()
345    }
346
347    /// Get number of pending broadcasts
348    pub fn pending_count(&self) -> usize {
349        self.pending.len()
350    }
351}
352
353impl Default for TimingCoordinator {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359/// Fingerprinting resistance analyzer
360pub struct FingerprintingAnalyzer;
361
362impl FingerprintingAnalyzer {
363    /// Analyze transaction for fingerprinting issues
364    #[allow(dead_code)]
365    pub fn analyze_fingerprints(
366        &self,
367        input_count: usize,
368        output_count: usize,
369        output_amounts: &[u64],
370    ) -> Vec<FingerprintingIssue> {
371        let mut issues = Vec::new();
372
373        // Check for exact round numbers
374        for amount in output_amounts {
375            if self.is_exact_round_number(*amount) {
376                issues.push(FingerprintingIssue::RoundNumber { amount: *amount });
377            }
378        }
379
380        // Check for common patterns
381        if output_count == 2 && input_count == 1 {
382            issues.push(FingerprintingIssue::SimplePayment);
383        }
384
385        // Check for duplicate amounts
386        let mut amount_counts: HashMap<u64, usize> = HashMap::new();
387        for amount in output_amounts {
388            *amount_counts.entry(*amount).or_insert(0) += 1;
389        }
390
391        for (amount, count) in amount_counts {
392            if count > 1 {
393                issues.push(FingerprintingIssue::DuplicateAmount { amount, count });
394            }
395        }
396
397        issues
398    }
399
400    /// Check if amount is an exact round number
401    fn is_exact_round_number(&self, amount: u64) -> bool {
402        if amount == 0 {
403            return false;
404        }
405
406        // Check if divisible by 1 BTC, 0.1 BTC, 0.01 BTC, etc.
407        let btc = 100_000_000u64;
408        for divisor in [btc, btc / 10, btc / 100, btc / 1000] {
409            if amount % divisor == 0 {
410                return true;
411            }
412        }
413
414        false
415    }
416}
417
418/// Fingerprinting issues detected
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub enum FingerprintingIssue {
421    /// Amount is a round number
422    RoundNumber { amount: u64 },
423    /// Simple 1-input, 2-output pattern
424    SimplePayment,
425    /// Duplicate output amounts
426    DuplicateAmount { amount: u64, count: usize },
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_amount_obfuscation_none() {
435        let enhancer = TransactionPrivacyEnhancer::new(
436            StructureRandomization::default(),
437            AmountObfuscation::None,
438            TimingObfuscation::default(),
439        );
440
441        assert_eq!(enhancer.obfuscate_amount(12345), 12345);
442    }
443
444    #[test]
445    fn test_amount_obfuscation_round() {
446        let enhancer = TransactionPrivacyEnhancer::new(
447            StructureRandomization::default(),
448            AmountObfuscation::RoundPowerOfTen,
449            TimingObfuscation::default(),
450        );
451
452        let result = enhancer.obfuscate_amount(12345);
453        assert!(result % 10000 == 0);
454    }
455
456    #[test]
457    fn test_change_output_standard() {
458        let generator = ChangeOutputGenerator::new(ChangeStrategy::Standard, 546);
459        let outputs = generator.generate_change_outputs(100_000, None).unwrap();
460
461        assert_eq!(outputs.len(), 1);
462        assert_eq!(outputs[0], 100_000);
463    }
464
465    #[test]
466    fn test_change_output_multiple() {
467        let generator = ChangeOutputGenerator::new(ChangeStrategy::Multiple { count: 2 }, 546);
468        let outputs = generator.generate_change_outputs(100_000, None).unwrap();
469
470        assert_eq!(outputs.len(), 2);
471        assert_eq!(outputs.iter().sum::<u64>(), 100_000);
472    }
473
474    #[test]
475    fn test_timing_coordinator() {
476        let mut coordinator = TimingCoordinator::new();
477
478        coordinator.schedule_broadcast(
479            "tx1".to_string(),
480            "hex1".to_string(),
481            0,
482            BroadcastPriority::Normal,
483        );
484
485        assert_eq!(coordinator.pending_count(), 1);
486
487        let ready = coordinator.get_ready_broadcasts();
488        assert_eq!(ready.len(), 1);
489        assert_eq!(ready[0].0, "tx1");
490    }
491
492    #[test]
493    fn test_fingerprinting_analyzer() {
494        let analyzer = FingerprintingAnalyzer;
495        let issues = analyzer.analyze_fingerprints(
496            1,
497            2,
498            &[100_000_000, 50_000_000], // 1 BTC and 0.5 BTC
499        );
500
501        assert!(!issues.is_empty());
502    }
503
504    #[test]
505    fn test_broadcast_priority() {
506        let low = BroadcastPriority::Low;
507        let high = BroadcastPriority::High;
508
509        assert_ne!(low, high);
510    }
511
512    #[test]
513    fn test_structure_randomization_default() {
514        let config = StructureRandomization::default();
515        assert!(config.add_decoy_outputs);
516        assert!(config.randomize_output_order);
517        assert!(config.randomize_input_order);
518    }
519}