1use crate::btc_utils::round_for_privacy;
7use crate::error::BitcoinError;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct StructureRandomization {
14 pub add_decoy_outputs: bool,
16 pub randomize_output_order: bool,
18 pub randomize_input_order: bool,
20 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#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum AmountObfuscation {
38 None,
40 RoundPowerOfTen,
42 RoundDenomination { sats: u64 },
44 AddRandomDust { max_dust: u64 },
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TimingObfuscation {
51 pub random_delay_secs: Option<(u64, u64)>, pub broadcast_hour: Option<u8>,
55 pub batch_broadcast: bool,
57}
58
59impl Default for TimingObfuscation {
60 fn default() -> Self {
61 Self {
62 random_delay_secs: Some((0, 300)), broadcast_hour: None,
64 batch_broadcast: false,
65 }
66 }
67}
68
69pub struct TransactionPrivacyEnhancer {
71 structure_randomization: StructureRandomization,
72 amount_obfuscation: AmountObfuscation,
73 timing_obfuscation: TimingObfuscation,
74}
75
76impl TransactionPrivacyEnhancer {
77 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 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 pub fn generate_decoy_amount(&self, total_amount: u64) -> u64 {
114 use rand::Rng;
115 let mut rng = rand::rng();
116 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 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) }
132 }
133
134 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum ChangeStrategy {
164 Standard,
166 Multiple { count: usize },
168 MatchPayment,
170 RandomSplit,
172}
173
174pub struct ChangeOutputGenerator {
176 strategy: ChangeStrategy,
177 min_change: u64,
178}
179
180impl ChangeOutputGenerator {
181 pub fn new(strategy: ChangeStrategy, min_change: u64) -> Self {
183 Self {
184 strategy,
185 min_change,
186 }
187 }
188
189 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 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 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 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
265pub struct TimingCoordinator {
267 pending: HashMap<String, PendingBroadcast>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct PendingBroadcast {
274 pub tx_hex: String,
276 pub broadcast_at: chrono::DateTime<chrono::Utc>,
278 pub priority: BroadcastPriority,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284pub enum BroadcastPriority {
285 Low,
287 Normal,
289 High,
291}
292
293impl TimingCoordinator {
294 pub fn new() -> Self {
296 Self {
297 pending: HashMap::new(),
298 }
299 }
300
301 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 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 pub fn cancel_broadcast(&mut self, tx_id: &str) -> bool {
344 self.pending.remove(tx_id).is_some()
345 }
346
347 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
359pub struct FingerprintingAnalyzer;
361
362impl FingerprintingAnalyzer {
363 #[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 for amount in output_amounts {
375 if self.is_exact_round_number(*amount) {
376 issues.push(FingerprintingIssue::RoundNumber { amount: *amount });
377 }
378 }
379
380 if output_count == 2 && input_count == 1 {
382 issues.push(FingerprintingIssue::SimplePayment);
383 }
384
385 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 fn is_exact_round_number(&self, amount: u64) -> bool {
402 if amount == 0 {
403 return false;
404 }
405
406 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#[derive(Debug, Clone, Serialize, Deserialize)]
420pub enum FingerprintingIssue {
421 RoundNumber { amount: u64 },
423 SimplePayment,
425 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], );
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}