use crate::btc_utils::round_for_privacy;
use crate::error::BitcoinError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructureRandomization {
pub add_decoy_outputs: bool,
pub randomize_output_order: bool,
pub randomize_input_order: bool,
pub randomize_nsequence: bool,
}
impl Default for StructureRandomization {
fn default() -> Self {
Self {
add_decoy_outputs: true,
randomize_output_order: true,
randomize_input_order: true,
randomize_nsequence: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AmountObfuscation {
None,
RoundPowerOfTen,
RoundDenomination {
sats: u64,
},
AddRandomDust {
max_dust: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimingObfuscation {
pub random_delay_secs: Option<(u64, u64)>, pub broadcast_hour: Option<u8>,
pub batch_broadcast: bool,
}
impl Default for TimingObfuscation {
fn default() -> Self {
Self {
random_delay_secs: Some((0, 300)), broadcast_hour: None,
batch_broadcast: false,
}
}
}
pub struct TransactionPrivacyEnhancer {
structure_randomization: StructureRandomization,
amount_obfuscation: AmountObfuscation,
timing_obfuscation: TimingObfuscation,
}
impl TransactionPrivacyEnhancer {
pub fn new(
structure_randomization: StructureRandomization,
amount_obfuscation: AmountObfuscation,
timing_obfuscation: TimingObfuscation,
) -> Self {
Self {
structure_randomization,
amount_obfuscation,
timing_obfuscation,
}
}
pub fn obfuscate_amount(&self, amount: u64) -> u64 {
match &self.amount_obfuscation {
AmountObfuscation::None => amount,
AmountObfuscation::RoundPowerOfTen => round_for_privacy(amount, 10_000),
AmountObfuscation::RoundDenomination { sats } => {
let remainder = amount % sats;
if remainder < sats / 2 {
amount - remainder
} else {
amount + (sats - remainder)
}
}
AmountObfuscation::AddRandomDust { max_dust } => {
use rand::RngExt;
let mut rng = rand::rng();
let dust = rng.random_range(0..*max_dust);
amount + dust
}
}
}
pub fn generate_decoy_amount(&self, total_amount: u64) -> u64 {
use rand::RngExt;
let mut rng = rand::rng();
let min = total_amount / 100;
let max = total_amount / 5;
if min >= max {
return min;
}
rng.random_range(min..=max)
}
pub fn should_add_decoy(&self) -> bool {
use rand::RngExt;
self.structure_randomization.add_decoy_outputs && {
let mut rng = rand::rng();
rng.random_bool(0.3) }
}
pub fn calculate_broadcast_delay(&self) -> u64 {
use rand::RngExt;
if let Some((min, max)) = self.timing_obfuscation.random_delay_secs {
let mut rng = rand::rng();
rng.random_range(min..=max)
} else {
0
}
}
pub fn should_batch(&self) -> bool {
self.timing_obfuscation.batch_broadcast
}
}
impl Default for TransactionPrivacyEnhancer {
fn default() -> Self {
Self::new(
StructureRandomization::default(),
AmountObfuscation::RoundPowerOfTen,
TimingObfuscation::default(),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChangeStrategy {
Standard,
Multiple {
count: usize,
},
MatchPayment,
RandomSplit,
}
pub struct ChangeOutputGenerator {
strategy: ChangeStrategy,
min_change: u64,
}
impl ChangeOutputGenerator {
pub fn new(strategy: ChangeStrategy, min_change: u64) -> Self {
Self {
strategy,
min_change,
}
}
pub fn generate_change_outputs(
&self,
total_change: u64,
payment_amount: Option<u64>,
) -> Result<Vec<u64>, BitcoinError> {
if total_change < self.min_change {
return Ok(Vec::new());
}
match &self.strategy {
ChangeStrategy::Standard => Ok(vec![total_change]),
ChangeStrategy::Multiple { count } => self.split_change_multiple(total_change, *count),
ChangeStrategy::MatchPayment => {
if let Some(payment) = payment_amount {
Ok(vec![payment, total_change.saturating_sub(payment)]).map(|outputs| {
if outputs[1] < self.min_change {
vec![total_change]
} else {
outputs
}
})
} else {
Ok(vec![total_change])
}
}
ChangeStrategy::RandomSplit => self.split_change_random(total_change),
}
}
fn split_change_multiple(&self, total: u64, count: usize) -> Result<Vec<u64>, BitcoinError> {
if count == 0 {
return Err(BitcoinError::InvalidTransaction(
"Count must be greater than 0".to_string(),
));
}
if count == 1 {
return Ok(vec![total]);
}
let min_per_output = self.min_change;
if total < min_per_output * count as u64 {
return Ok(vec![total]);
}
use rand::RngExt;
let mut outputs = Vec::new();
let mut remaining = total;
let mut rng = rand::rng();
for i in 0..count {
if i == count - 1 {
outputs.push(remaining);
} else {
let max_amount = remaining - (min_per_output * (count - i - 1) as u64);
let amount = rng.random_range(min_per_output..=max_amount);
outputs.push(amount);
remaining -= amount;
}
}
Ok(outputs)
}
fn split_change_random(&self, total: u64) -> Result<Vec<u64>, BitcoinError> {
use rand::RngExt;
let mut rng = rand::rng();
let count = rng.random_range(1..=3);
self.split_change_multiple(total, count)
}
}
pub struct TimingCoordinator {
pending: HashMap<String, PendingBroadcast>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingBroadcast {
pub tx_hex: String,
pub broadcast_at: chrono::DateTime<chrono::Utc>,
pub priority: BroadcastPriority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BroadcastPriority {
Low,
Normal,
High,
}
impl TimingCoordinator {
pub fn new() -> Self {
Self {
pending: HashMap::new(),
}
}
pub fn schedule_broadcast(
&mut self,
tx_id: String,
tx_hex: String,
delay_secs: u64,
priority: BroadcastPriority,
) {
let broadcast_at = chrono::Utc::now() + chrono::Duration::seconds(delay_secs as i64);
self.pending.insert(
tx_id,
PendingBroadcast {
tx_hex,
broadcast_at,
priority,
},
);
}
pub fn get_ready_broadcasts(&mut self) -> Vec<(String, String)> {
let now = chrono::Utc::now();
let mut ready = Vec::new();
let ready_ids: Vec<String> = self
.pending
.iter()
.filter(|(_, pending)| pending.broadcast_at <= now)
.map(|(id, _)| id.clone())
.collect();
for id in ready_ids {
if let Some(pending) = self.pending.remove(&id) {
ready.push((id, pending.tx_hex));
}
}
ready
}
pub fn cancel_broadcast(&mut self, tx_id: &str) -> bool {
self.pending.remove(tx_id).is_some()
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
impl Default for TimingCoordinator {
fn default() -> Self {
Self::new()
}
}
pub struct FingerprintingAnalyzer;
impl FingerprintingAnalyzer {
#[allow(dead_code)]
pub fn analyze_fingerprints(
&self,
input_count: usize,
output_count: usize,
output_amounts: &[u64],
) -> Vec<FingerprintingIssue> {
let mut issues = Vec::new();
for amount in output_amounts {
if self.is_exact_round_number(*amount) {
issues.push(FingerprintingIssue::RoundNumber { amount: *amount });
}
}
if output_count == 2 && input_count == 1 {
issues.push(FingerprintingIssue::SimplePayment);
}
let mut amount_counts: HashMap<u64, usize> = HashMap::new();
for amount in output_amounts {
*amount_counts.entry(*amount).or_insert(0) += 1;
}
for (amount, count) in amount_counts {
if count > 1 {
issues.push(FingerprintingIssue::DuplicateAmount { amount, count });
}
}
issues
}
fn is_exact_round_number(&self, amount: u64) -> bool {
if amount == 0 {
return false;
}
let btc = 100_000_000u64;
for divisor in [btc, btc / 10, btc / 100, btc / 1000] {
if amount % divisor == 0 {
return true;
}
}
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FingerprintingIssue {
RoundNumber {
amount: u64,
},
SimplePayment,
DuplicateAmount {
amount: u64,
count: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_amount_obfuscation_none() {
let enhancer = TransactionPrivacyEnhancer::new(
StructureRandomization::default(),
AmountObfuscation::None,
TimingObfuscation::default(),
);
assert_eq!(enhancer.obfuscate_amount(12345), 12345);
}
#[test]
fn test_amount_obfuscation_round() {
let enhancer = TransactionPrivacyEnhancer::new(
StructureRandomization::default(),
AmountObfuscation::RoundPowerOfTen,
TimingObfuscation::default(),
);
let result = enhancer.obfuscate_amount(12345);
assert!(result % 10000 == 0);
}
#[test]
fn test_change_output_standard() {
let generator = ChangeOutputGenerator::new(ChangeStrategy::Standard, 546);
let outputs = generator.generate_change_outputs(100_000, None).unwrap();
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0], 100_000);
}
#[test]
fn test_change_output_multiple() {
let generator = ChangeOutputGenerator::new(ChangeStrategy::Multiple { count: 2 }, 546);
let outputs = generator.generate_change_outputs(100_000, None).unwrap();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs.iter().sum::<u64>(), 100_000);
}
#[test]
fn test_timing_coordinator() {
let mut coordinator = TimingCoordinator::new();
coordinator.schedule_broadcast(
"tx1".to_string(),
"hex1".to_string(),
0,
BroadcastPriority::Normal,
);
assert_eq!(coordinator.pending_count(), 1);
let ready = coordinator.get_ready_broadcasts();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].0, "tx1");
}
#[test]
fn test_fingerprinting_analyzer() {
let analyzer = FingerprintingAnalyzer;
let issues = analyzer.analyze_fingerprints(
1,
2,
&[100_000_000, 50_000_000], );
assert!(!issues.is_empty());
}
#[test]
fn test_broadcast_priority() {
let low = BroadcastPriority::Low;
let high = BroadcastPriority::High;
assert_ne!(low, high);
}
#[test]
fn test_structure_randomization_default() {
let config = StructureRandomization::default();
assert!(config.add_decoy_outputs);
assert!(config.randomize_output_order);
assert!(config.randomize_input_order);
}
}