@@ -37,6 +37,7 @@ mod optional_parameters;
37
37
#[ cfg( feature = "v2" ) ]
38
38
pub mod v2;
39
39
40
+ use bitcoin:: secp256k1:: rand:: seq:: SliceRandom ;
40
41
use bitcoin:: secp256k1:: rand:: { self , Rng } ;
41
42
pub use error:: { Error , OutputSubstitutionError , RequestError , SelectionError } ;
42
43
use error:: {
@@ -382,62 +383,61 @@ impl WantsOutputs {
382
383
drain_script : & Script ,
383
384
) -> Result < WantsOutputs , OutputSubstitutionError > {
384
385
let mut payjoin_psbt = self . original_psbt . clone ( ) ;
385
- let mut change_vout: Option < usize > = None ;
386
- if self . params . disable_output_substitution {
387
- // Receiver may not change the script pubkey or decrease the value of the
388
- // their output, but can still increase the amount or add new outputs.
389
- // https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#payment-output-substitution
390
- let original_output = & self . original_psbt . unsigned_tx . output [ self . owned_vouts [ 0 ] ] ;
391
- match replacement_outputs
392
- . iter ( )
393
- . find ( |txo| txo. script_pubkey == original_output. script_pubkey )
394
- {
395
- Some ( txo) if txo. value < original_output. value => {
396
- return Err ( InternalOutputSubstitutionError :: OutputSubstitutionDisabled (
397
- "Decreasing the receiver output value is not allowed" ,
398
- )
399
- . into ( ) ) ;
400
- }
401
- None =>
402
- return Err ( InternalOutputSubstitutionError :: OutputSubstitutionDisabled (
403
- "Changing the receiver output script pubkey is not allowed" ,
404
- )
405
- . into ( ) ) ,
406
- _ => log:: info!( "Receiver is augmenting outputs" ) ,
407
- }
408
- }
409
386
let mut outputs = vec ! [ ] ;
410
387
let mut replacement_outputs = replacement_outputs. clone ( ) ;
411
388
let mut rng = rand:: thread_rng ( ) ;
412
- // Replace the existing receiver outputs
413
- for ( i, output ) in self . original_psbt . unsigned_tx . output . iter ( ) . enumerate ( ) {
389
+ // Substitute the existing receiver outputs, keeping the sender/receiver output ordering
390
+ for ( i, original_output ) in self . original_psbt . unsigned_tx . output . iter ( ) . enumerate ( ) {
414
391
if self . owned_vouts . contains ( & i) {
415
- // Receiver output: substitute with a provided output picked randomly
392
+ // Receiver output: substitute in-place a provided replacement output
416
393
if replacement_outputs. is_empty ( ) {
417
394
return Err ( InternalOutputSubstitutionError :: NotEnoughOutputs . into ( ) ) ;
418
395
}
419
- let index = rng. gen_range ( 0 ..replacement_outputs. len ( ) ) ;
420
- let txo = replacement_outputs. swap_remove ( index) ;
421
- if * drain_script == txo. script_pubkey {
422
- change_vout = Some ( i) ;
396
+ match replacement_outputs
397
+ . iter ( )
398
+ . position ( |txo| txo. script_pubkey == original_output. script_pubkey )
399
+ {
400
+ // Select an output with the same address if one was provided
401
+ Some ( pos) => {
402
+ let txo = replacement_outputs. swap_remove ( pos) ;
403
+ if self . params . disable_output_substitution
404
+ && txo. value < original_output. value
405
+ {
406
+ return Err (
407
+ InternalOutputSubstitutionError :: OutputSubstitutionDisabled (
408
+ "Decreasing the receiver output value is not allowed" ,
409
+ )
410
+ . into ( ) ,
411
+ ) ;
412
+ }
413
+ outputs. push ( txo) ;
414
+ }
415
+ // Otherwise randomly select one of the replacement outputs
416
+ None => {
417
+ if self . params . disable_output_substitution {
418
+ return Err (
419
+ InternalOutputSubstitutionError :: OutputSubstitutionDisabled (
420
+ "Changing the receiver output script pubkey is not allowed" ,
421
+ )
422
+ . into ( ) ,
423
+ ) ;
424
+ }
425
+ let index = rng. gen_range ( 0 ..replacement_outputs. len ( ) ) ;
426
+ let txo = replacement_outputs. swap_remove ( index) ;
427
+ outputs. push ( txo) ;
428
+ }
423
429
}
424
- outputs. push ( txo) ;
425
430
} else {
426
431
// Sender output: leave it as is
427
- outputs. push ( output. clone ( ) ) ;
428
- }
429
- }
430
- // Append all remaining outputs
431
- while !replacement_outputs. is_empty ( ) {
432
- let index = rng. gen_range ( 0 ..replacement_outputs. len ( ) ) ;
433
- let txo = replacement_outputs. swap_remove ( index) ;
434
- if * drain_script == txo. script_pubkey {
435
- change_vout = Some ( outputs. len ( ) ) ;
432
+ outputs. push ( original_output. clone ( ) ) ;
436
433
}
437
- outputs. push ( txo) ;
438
- // Additional outputs must also be added to the PSBT outputs data structure
439
- payjoin_psbt. outputs . push ( Default :: default ( ) ) ;
440
434
}
435
+ // Insert all remaining outputs at random indices for privacy
436
+ interleave_shuffle ( & mut outputs, & mut replacement_outputs, & mut rng) ;
437
+ // Identify the receiver output that will be used for change and fees
438
+ let change_vout = outputs. iter ( ) . position ( |txo| txo. script_pubkey == * drain_script) ;
439
+ // Update the payjoin PSBT outputs
440
+ payjoin_psbt. outputs = vec ! [ Default :: default ( ) ; outputs. len( ) ] ;
441
441
payjoin_psbt. unsigned_tx . output = outputs;
442
442
Ok ( WantsOutputs {
443
443
original_psbt : self . original_psbt ,
@@ -460,6 +460,34 @@ impl WantsOutputs {
460
460
}
461
461
}
462
462
463
+ /// Shuffles `new` vector, then interleaves its elements with those from `original`,
464
+ /// maintaining the relative order in `original` but randomly inserting elements from `new`.
465
+ /// The combined result replaces the contents of `original`.
466
+ fn interleave_shuffle < T : Clone , R : rand:: Rng > (
467
+ original : & mut Vec < T > ,
468
+ new : & mut Vec < T > ,
469
+ rng : & mut R ,
470
+ ) {
471
+ // Shuffle the substitute_outputs
472
+ new. shuffle ( rng) ;
473
+ // Create a new vector to store the combined result
474
+ let mut combined = Vec :: with_capacity ( original. len ( ) + new. len ( ) ) ;
475
+ // Initialize indices
476
+ let mut original_index = 0 ;
477
+ let mut new_index = 0 ;
478
+ // Interleave elements
479
+ while original_index < original. len ( ) || new_index < new. len ( ) {
480
+ if original_index < original. len ( ) && ( new_index >= new. len ( ) || rng. gen_bool ( 0.5 ) ) {
481
+ combined. push ( original[ original_index] . clone ( ) ) ;
482
+ original_index += 1 ;
483
+ } else {
484
+ combined. push ( new[ new_index] . clone ( ) ) ;
485
+ new_index += 1 ;
486
+ }
487
+ }
488
+ * original = combined;
489
+ }
490
+
463
491
/// A checked proposal that the receiver may contribute inputs to to make a payjoin
464
492
#[ derive( Debug , Clone ) ]
465
493
pub struct WantsInputs {
@@ -840,6 +868,9 @@ impl PayjoinProposal {
840
868
841
869
#[ cfg( test) ]
842
870
mod test {
871
+ use rand:: rngs:: StdRng ;
872
+ use rand:: SeedableRng ;
873
+
843
874
use super :: * ;
844
875
845
876
struct MockHeaders {
@@ -915,4 +946,26 @@ mod test {
915
946
916
947
assert ! ( payjoin. is_ok( ) , "Payjoin should be a valid PSBT" ) ;
917
948
}
949
+
950
+ #[ test]
951
+ fn test_interleave_shuffle ( ) {
952
+ let mut original1 = vec ! [ 1 , 2 , 3 ] ;
953
+ let mut original2 = original1. clone ( ) ;
954
+ let mut original3 = original1. clone ( ) ;
955
+ let mut new1 = vec ! [ 4 , 5 , 6 ] ;
956
+ let mut new2 = new1. clone ( ) ;
957
+ let mut new3 = new1. clone ( ) ;
958
+ let mut rng1 = StdRng :: seed_from_u64 ( 123 ) ;
959
+ let mut rng2 = StdRng :: seed_from_u64 ( 234 ) ;
960
+ let mut rng3 = StdRng :: seed_from_u64 ( 345 ) ;
961
+ // Operate on the same data multiple times with different RNG seeds.
962
+ interleave_shuffle ( & mut original1, & mut new1, & mut rng1) ;
963
+ interleave_shuffle ( & mut original2, & mut new2, & mut rng2) ;
964
+ interleave_shuffle ( & mut original3, & mut new3, & mut rng3) ;
965
+ // The result should be different for each seed
966
+ // and the relative ordering from `original` always preserved/
967
+ assert_eq ! ( original1, vec![ 1 , 6 , 2 , 5 , 4 , 3 ] ) ;
968
+ assert_eq ! ( original2, vec![ 1 , 5 , 4 , 2 , 6 , 3 ] ) ;
969
+ assert_eq ! ( original3, vec![ 4 , 5 , 1 , 2 , 6 , 3 ] ) ;
970
+ }
918
971
}
0 commit comments