rustywallet_coinjoin/
lib.rs

1//! # rustywallet-coinjoin
2//!
3//! CoinJoin and PayJoin (BIP78) utilities for rustywallet.
4//!
5//! This crate provides tools for building privacy-enhancing Bitcoin transactions:
6//!
7//! - **PayJoin (BIP78)**: Receiver contributes inputs to break common-input-ownership heuristic
8//! - **CoinJoin**: Multiple users combine transactions with equal outputs
9//! - **Output Mixing**: Shuffle and equalize outputs for privacy
10//! - **Coordinator-less Protocol**: P2P CoinJoin without central coordinator
11//!
12//! ## Quick Start
13//!
14//! ### PayJoin (BIP78)
15//!
16//! ```rust
17//! use rustywallet_coinjoin::prelude::*;
18//!
19//! // Receiver creates PayJoin request
20//! let mut receiver = PayJoinReceiver::new(vec![0x00, 0x14], 100_000);
21//! receiver.add_utxo(InputRef::from_outpoint([1u8; 32], 0, 50_000));
22//!
23//! let request = receiver.create_request("cHNidP8...").unwrap();
24//! println!("Receiver inputs: {}", request.receiver_inputs.len());
25//! ```
26//!
27//! ### CoinJoin Transaction
28//!
29//! ```rust
30//! use rustywallet_coinjoin::prelude::*;
31//!
32//! let mut builder = CoinJoinBuilder::new();
33//!
34//! // Add participants
35//! builder.add_participant_simple(
36//!     "alice",
37//!     vec![InputRef::from_outpoint([1u8; 32], 0, 100_000)],
38//!     vec![0x00, 0x14, 0x01],
39//! );
40//! builder.add_participant_simple(
41//!     "bob",
42//!     vec![InputRef::from_outpoint([2u8; 32], 0, 100_000)],
43//!     vec![0x00, 0x14, 0x02],
44//! );
45//!
46//! builder.set_output_amount(50_000);
47//! let tx = builder.build().unwrap();
48//!
49//! assert!(tx.verify_equal_outputs());
50//! ```
51//!
52//! ### Coordinator-less Session
53//!
54//! ```rust
55//! use rustywallet_coinjoin::prelude::*;
56//!
57//! // Create session
58//! let mut session = CoinJoinSession::new(50_000);
59//!
60//! // Participants join
61//! let alice = Participant::new(
62//!     "alice",
63//!     vec![InputRef::from_outpoint([1u8; 32], 0, 100_000)],
64//!     vec![0x00, 0x14],
65//! );
66//! session.join(alice).unwrap();
67//!
68//! let bob = Participant::new(
69//!     "bob",
70//!     vec![InputRef::from_outpoint([2u8; 32], 0, 100_000)],
71//!     vec![0x00, 0x14],
72//! );
73//! session.join(bob).unwrap();
74//!
75//! // Build transaction
76//! let tx = session.build_transaction().unwrap();
77//! ```
78//!
79//! ## Privacy Considerations
80//!
81//! - Use equal output amounts to maximize anonymity set
82//! - Shuffle inputs and outputs to hide ownership
83//! - Avoid unique change amounts that can be linked
84//! - Use standard denominations when possible
85//!
86//! ## Security
87//!
88//! - Verify all inputs before signing
89//! - Check fee calculations
90//! - Validate output amounts match expectations
91//! - Use commitments to prevent manipulation
92
93pub mod builder;
94pub mod coordinator;
95pub mod error;
96pub mod mixer;
97pub mod payjoin;
98pub mod prelude;
99pub mod types;
100
101pub use builder::{CoinJoinBuilder, CoinJoinTransaction};
102pub use coordinator::{CoinJoinSession, JoinResponse, SessionAnnouncement, SessionState};
103pub use error::{CoinJoinError, Result};
104pub use mixer::{analyze_privacy, find_best_denomination, OutputMixer, PrivacyAnalysis};
105pub use payjoin::{PayJoinProposal, PayJoinReceiver, PayJoinRequest, PayJoinSender};
106pub use types::{FeeStrategy, InputRef, OutputDef, Participant};
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_full_coinjoin_workflow() {
114        // Create CoinJoin with 3 participants
115        let mut builder = CoinJoinBuilder::new();
116
117        builder.add_participant_simple(
118            "alice",
119            vec![InputRef::from_outpoint([1u8; 32], 0, 100_000)],
120            vec![0x00, 0x14, 0x01],
121        );
122        builder.add_participant_simple(
123            "bob",
124            vec![InputRef::from_outpoint([2u8; 32], 0, 100_000)],
125            vec![0x00, 0x14, 0x02],
126        );
127        builder.add_participant_simple(
128            "carol",
129            vec![InputRef::from_outpoint([3u8; 32], 0, 100_000)],
130            vec![0x00, 0x14, 0x03],
131        );
132
133        builder.set_output_amount(50_000);
134        builder.set_fee_rate(1.0);
135
136        let tx = builder.build().unwrap();
137
138        // Verify
139        assert_eq!(tx.participant_count, 3);
140        assert_eq!(tx.inputs.len(), 3);
141        assert_eq!(tx.outputs.len(), 3);
142        assert!(tx.verify_equal_outputs());
143        assert_eq!(tx.output_amount, 50_000);
144
145        // Analyze privacy
146        let analysis = analyze_privacy(&tx.outputs);
147        assert_eq!(analysis.anonymity_set, 3);
148        assert!(!analysis.has_change);
149    }
150
151    #[test]
152    fn test_payjoin_workflow() {
153        // Receiver setup
154        let mut receiver = PayJoinReceiver::new(vec![0x00, 0x14], 100_000);
155        receiver.add_utxo(InputRef::from_outpoint([1u8; 32], 0, 50_000));
156
157        // Create request
158        let request = receiver.create_request("cHNidP8...").unwrap();
159        assert_eq!(request.receiver_inputs.len(), 1);
160
161        // Sender processes
162        let mut sender = PayJoinSender::new();
163        sender.add_utxo(InputRef::from_outpoint([2u8; 32], 0, 150_000));
164
165        let proposal = sender.process_request(&request).unwrap();
166        assert_eq!(proposal.input_count(), 2);
167    }
168
169    #[test]
170    fn test_session_workflow() {
171        let mut session = CoinJoinSession::new(50_000);
172
173        // Join participants
174        let alice = Participant::new(
175            "alice",
176            vec![InputRef::from_outpoint([1u8; 32], 0, 100_000)],
177            vec![0x00, 0x14, 0x01],
178        );
179        let bob = Participant::new(
180            "bob",
181            vec![InputRef::from_outpoint([2u8; 32], 0, 100_000)],
182            vec![0x00, 0x14, 0x02],
183        );
184
185        session.join(alice).unwrap();
186        let response = session.join(bob).unwrap();
187        assert!(response.ready);
188
189        // Build
190        let tx = session.build_transaction().unwrap();
191        assert_eq!(tx.participant_count, 2);
192
193        // Sign
194        session.submit_signature("alice", vec![1, 2, 3]).unwrap();
195        session.submit_signature("bob", vec![4, 5, 6]).unwrap();
196
197        assert!(session.is_complete());
198    }
199
200    #[test]
201    fn test_denominations() {
202        // Find best denomination
203        let denom = find_best_denomination(150_000, 1000);
204        assert_eq!(denom, Some(100_000));
205
206        // Split into denominations
207        let splits = mixer::split_into_denominations(350_000, 1000);
208        let total: u64 = splits.iter().sum();
209        assert!(total <= 350_000);
210    }
211
212    #[test]
213    fn test_output_mixing() {
214        let mut mixer = OutputMixer::new();
215
216        for i in 0..5 {
217            mixer.add_output(OutputDef::new(50_000, vec![i]));
218        }
219
220        // Verify equal
221        let amount = mixer.verify_equal().unwrap();
222        assert_eq!(amount, 50_000);
223
224        // Shuffle
225        mixer.set_seed([42u8; 32]);
226        let shuffled = mixer.shuffle();
227        assert_eq!(shuffled.len(), 5);
228    }
229}