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}