rustywallet_coinjoin/
mixer.rs1use crate::error::{CoinJoinError, Result};
4use crate::types::OutputDef;
5use sha2::{Digest, Sha256};
6
7pub const DENOMINATIONS: &[u64] = &[
9 10_000, 50_000, 100_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 50_000_000, 100_000_000, ];
19
20pub 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
29pub 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
44pub struct OutputMixer {
46 outputs: Vec<OutputDef>,
48 seed: Option<[u8; 32]>,
50}
51
52impl OutputMixer {
53 pub fn new() -> Self {
55 Self {
56 outputs: Vec::new(),
57 seed: None,
58 }
59 }
60
61 pub fn add_output(&mut self, output: OutputDef) {
63 self.outputs.push(output);
64 }
65
66 pub fn add_outputs(&mut self, outputs: impl IntoIterator<Item = OutputDef>) {
68 self.outputs.extend(outputs);
69 }
70
71 pub fn set_seed(&mut self, seed: [u8; 32]) {
73 self.seed = Some(seed);
74 }
75
76 pub fn shuffle(&mut self) -> &[OutputDef] {
78 let seed = self.seed.unwrap_or_else(|| {
79 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 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 pub fn outputs(&self) -> &[OutputDef] {
110 &self.outputs
111 }
112
113 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#[derive(Debug, Clone)]
141pub struct PrivacyAnalysis {
142 pub equal_outputs: usize,
144 pub unique_amounts: usize,
146 pub anonymity_set: usize,
148 pub has_change: bool,
150 pub score: u8,
152}
153
154pub 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 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 let max_equal = amounts.values().max().copied().unwrap_or(0);
174 let unique_amounts = amounts.len();
175
176 let has_change = amounts.values().any(|&count| count == 1);
178
179 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 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 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}