rustywallet_coinjoin/
mixer.rs

1//! Output mixing utilities.
2
3use crate::error::{CoinJoinError, Result};
4use crate::types::OutputDef;
5use sha2::{Digest, Sha256};
6
7/// Standard CoinJoin denominations (in satoshis).
8pub const DENOMINATIONS: &[u64] = &[
9    10_000,      // 0.0001 BTC
10    50_000,      // 0.0005 BTC
11    100_000,     // 0.001 BTC
12    500_000,     // 0.005 BTC
13    1_000_000,   // 0.01 BTC
14    5_000_000,   // 0.05 BTC
15    10_000_000,  // 0.1 BTC
16    50_000_000,  // 0.5 BTC
17    100_000_000, // 1 BTC
18];
19
20/// Find the best denomination for a given amount.
21pub fn find_best_denomination(amount: u64, min_change: u64) -> Option<u64> {
22    DENOMINATIONS
23        .iter()
24        .rev()
25        .find(|&&d| d <= amount && (amount - d) >= min_change || amount == d)
26        .copied()
27}
28
29/// Calculate how to split an amount into denominations.
30pub fn split_into_denominations(amount: u64, min_change: u64) -> Vec<u64> {
31    let mut remaining = amount;
32    let mut result = Vec::new();
33
34    for &denom in DENOMINATIONS.iter().rev() {
35        while remaining >= denom + min_change || remaining == denom {
36            result.push(denom);
37            remaining -= denom;
38        }
39    }
40
41    result
42}
43
44/// Output mixer for shuffling outputs.
45pub struct OutputMixer {
46    /// Outputs to mix
47    outputs: Vec<OutputDef>,
48    /// Shuffle seed
49    seed: Option<[u8; 32]>,
50}
51
52impl OutputMixer {
53    /// Create a new output mixer.
54    pub fn new() -> Self {
55        Self {
56            outputs: Vec::new(),
57            seed: None,
58        }
59    }
60
61    /// Add an output.
62    pub fn add_output(&mut self, output: OutputDef) {
63        self.outputs.push(output);
64    }
65
66    /// Add multiple outputs.
67    pub fn add_outputs(&mut self, outputs: impl IntoIterator<Item = OutputDef>) {
68        self.outputs.extend(outputs);
69    }
70
71    /// Set shuffle seed for deterministic shuffling.
72    pub fn set_seed(&mut self, seed: [u8; 32]) {
73        self.seed = Some(seed);
74    }
75
76    /// Shuffle outputs.
77    pub fn shuffle(&mut self) -> &[OutputDef] {
78        let seed = self.seed.unwrap_or_else(|| {
79            // Generate random seed
80            let mut hasher = Sha256::new();
81            hasher.update(
82                std::time::SystemTime::now()
83                    .duration_since(std::time::UNIX_EPOCH)
84                    .unwrap_or_default()
85                    .as_nanos()
86                    .to_le_bytes(),
87            );
88            let result = hasher.finalize();
89            let mut s = [0u8; 32];
90            s.copy_from_slice(&result);
91            s
92        });
93
94        // Fisher-Yates shuffle with deterministic randomness
95        let n = self.outputs.len();
96        for i in 0..n {
97            let mut hasher = Sha256::new();
98            hasher.update(seed);
99            hasher.update(i.to_le_bytes());
100            let hash = hasher.finalize();
101            let j = i + (u64::from_le_bytes(hash[0..8].try_into().unwrap()) as usize % (n - i));
102            self.outputs.swap(i, j);
103        }
104
105        &self.outputs
106    }
107
108    /// Get outputs.
109    pub fn outputs(&self) -> &[OutputDef] {
110        &self.outputs
111    }
112
113    /// Verify all outputs have equal amounts.
114    pub fn verify_equal(&self) -> Result<u64> {
115        if self.outputs.is_empty() {
116            return Err(CoinJoinError::InvalidOutput("No outputs".into()));
117        }
118
119        let amount = self.outputs[0].amount;
120        for output in &self.outputs[1..] {
121            if output.amount != amount {
122                return Err(CoinJoinError::UnequalOutputs {
123                    expected: amount,
124                    actual: output.amount,
125                });
126            }
127        }
128
129        Ok(amount)
130    }
131}
132
133impl Default for OutputMixer {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139/// Analyze outputs for privacy.
140#[derive(Debug, Clone)]
141pub struct PrivacyAnalysis {
142    /// Number of equal outputs
143    pub equal_outputs: usize,
144    /// Unique amounts
145    pub unique_amounts: usize,
146    /// Anonymity set size (equal outputs)
147    pub anonymity_set: usize,
148    /// Has change outputs (reduces privacy)
149    pub has_change: bool,
150    /// Privacy score (0-100)
151    pub score: u8,
152}
153
154/// Analyze a set of outputs for privacy.
155pub fn analyze_privacy(outputs: &[OutputDef]) -> PrivacyAnalysis {
156    if outputs.is_empty() {
157        return PrivacyAnalysis {
158            equal_outputs: 0,
159            unique_amounts: 0,
160            anonymity_set: 0,
161            has_change: false,
162            score: 0,
163        };
164    }
165
166    // Count amounts
167    let mut amounts: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
168    for output in outputs {
169        *amounts.entry(output.amount).or_insert(0) += 1;
170    }
171
172    // Find largest group of equal outputs
173    let max_equal = amounts.values().max().copied().unwrap_or(0);
174    let unique_amounts = amounts.len();
175
176    // Check for likely change outputs (unique amounts)
177    let has_change = amounts.values().any(|&count| count == 1);
178
179    // Calculate privacy score
180    let score = if outputs.len() <= 1 {
181        0
182    } else {
183        let equal_ratio = max_equal as f64 / outputs.len() as f64;
184        let base_score = (equal_ratio * 80.0) as u8;
185        let bonus = if max_equal >= 2 { 20 } else { 0 };
186        let penalty = if has_change { 10 } else { 0 };
187        (base_score + bonus).saturating_sub(penalty).min(100)
188    };
189
190    PrivacyAnalysis {
191        equal_outputs: max_equal,
192        unique_amounts,
193        anonymity_set: max_equal,
194        has_change,
195        score,
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_find_denomination() {
205        assert_eq!(find_best_denomination(150_000, 1000), Some(100_000));
206        assert_eq!(find_best_denomination(100_000, 0), Some(100_000));
207        assert_eq!(find_best_denomination(5_000, 1000), None);
208    }
209
210    #[test]
211    fn test_split_denominations() {
212        let splits = split_into_denominations(250_000, 1000);
213        assert!(splits.contains(&100_000));
214        assert!(splits.contains(&100_000));
215        assert!(splits.contains(&50_000));
216    }
217
218    #[test]
219    fn test_output_mixer() {
220        let mut mixer = OutputMixer::new();
221        mixer.add_output(OutputDef::new(50_000, vec![0x01]));
222        mixer.add_output(OutputDef::new(50_000, vec![0x02]));
223        mixer.add_output(OutputDef::new(50_000, vec![0x03]));
224
225        assert_eq!(mixer.outputs().len(), 3);
226        assert!(mixer.verify_equal().is_ok());
227    }
228
229    #[test]
230    fn test_mixer_shuffle_deterministic() {
231        let mut mixer1 = OutputMixer::new();
232        let mut mixer2 = OutputMixer::new();
233
234        for i in 0..5 {
235            mixer1.add_output(OutputDef::new(50_000, vec![i]));
236            mixer2.add_output(OutputDef::new(50_000, vec![i]));
237        }
238
239        let seed = [42u8; 32];
240        mixer1.set_seed(seed);
241        mixer2.set_seed(seed);
242
243        let shuffled1: Vec<_> = mixer1.shuffle().iter().map(|o| o.script_pubkey.clone()).collect();
244        let shuffled2: Vec<_> = mixer2.shuffle().iter().map(|o| o.script_pubkey.clone()).collect();
245
246        assert_eq!(shuffled1, shuffled2);
247    }
248
249    #[test]
250    fn test_verify_unequal() {
251        let mut mixer = OutputMixer::new();
252        mixer.add_output(OutputDef::new(50_000, vec![0x01]));
253        mixer.add_output(OutputDef::new(60_000, vec![0x02]));
254
255        assert!(mixer.verify_equal().is_err());
256    }
257
258    #[test]
259    fn test_privacy_analysis() {
260        // Good privacy: all equal
261        let outputs = vec![
262            OutputDef::new(50_000, vec![0x01]),
263            OutputDef::new(50_000, vec![0x02]),
264            OutputDef::new(50_000, vec![0x03]),
265        ];
266        let analysis = analyze_privacy(&outputs);
267        assert_eq!(analysis.equal_outputs, 3);
268        assert_eq!(analysis.anonymity_set, 3);
269        assert!(!analysis.has_change);
270        assert!(analysis.score >= 80);
271
272        // Poor privacy: unique amounts
273        let outputs = vec![
274            OutputDef::new(50_000, vec![0x01]),
275            OutputDef::new(60_000, vec![0x02]),
276            OutputDef::new(70_000, vec![0x03]),
277        ];
278        let analysis = analyze_privacy(&outputs);
279        assert_eq!(analysis.equal_outputs, 1);
280        assert!(analysis.has_change);
281        assert!(analysis.score < 50);
282    }
283}