|
25 | 25 | //! [reference implementation](https://github.com/payjoin/rust-payjoin/tree/master/payjoin-cli) |
26 | 26 |
|
27 | 27 | use std::cmp::{max, min}; |
| 28 | +use std::collections::BTreeMap; |
28 | 29 |
|
29 | 30 | use bitcoin::psbt::Psbt; |
30 | 31 | use bitcoin::secp256k1::rand::seq::SliceRandom; |
@@ -680,31 +681,42 @@ impl ProvisionalProposal { |
680 | 681 | output_contribution_weight |
681 | 682 | } |
682 | 683 |
|
683 | | - /// Prepare the PSBT by clearing the fields that the sender expects to be empty |
684 | | - fn prepare_psbt(mut self, processed_psbt: Psbt) -> PayjoinProposal { |
685 | | - self.payjoin_psbt = processed_psbt; |
686 | | - log::trace!("Preparing PSBT {:#?}", self.payjoin_psbt); |
687 | | - for output in self.payjoin_psbt.outputs_mut() { |
688 | | - output.bip32_derivation.clear(); |
689 | | - output.tap_key_origins.clear(); |
690 | | - output.tap_internal_key = None; |
691 | | - } |
692 | | - for input in self.payjoin_psbt.inputs_mut() { |
693 | | - input.bip32_derivation.clear(); |
694 | | - input.tap_key_origins.clear(); |
695 | | - input.tap_internal_key = None; |
696 | | - input.partial_sigs.clear(); |
| 684 | + /// Prepare the PSBT by creating a new PSBT and copying only the fields allowed by the [spec](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#senders-payjoin-proposal-checklist) |
| 685 | + fn prepare_psbt(self, processed_psbt: Psbt) -> PayjoinProposal { |
| 686 | + log::trace!("Original PSBT from callback: {processed_psbt:#?}"); |
| 687 | + |
| 688 | + // Create a new PSBT and copy only the allowed fields |
| 689 | + let mut filtered_psbt = Psbt { |
| 690 | + unsigned_tx: processed_psbt.unsigned_tx, |
| 691 | + version: processed_psbt.version, |
| 692 | + xpub: BTreeMap::new(), |
| 693 | + proprietary: BTreeMap::new(), |
| 694 | + unknown: BTreeMap::new(), |
| 695 | + inputs: vec![], |
| 696 | + outputs: vec![], |
| 697 | + }; |
| 698 | + |
| 699 | + for input in &processed_psbt.inputs { |
| 700 | + filtered_psbt.inputs.push(bitcoin::psbt::Input { |
| 701 | + witness_utxo: input.witness_utxo.clone(), |
| 702 | + non_witness_utxo: input.non_witness_utxo.clone(), |
| 703 | + sighash_type: input.sighash_type, |
| 704 | + final_script_sig: input.final_script_sig.clone(), |
| 705 | + final_script_witness: input.final_script_witness.clone(), |
| 706 | + tap_key_sig: input.tap_key_sig, |
| 707 | + tap_script_sigs: input.tap_script_sigs.clone(), |
| 708 | + tap_merkle_root: input.tap_merkle_root, |
| 709 | + ..Default::default() |
| 710 | + }); |
697 | 711 | } |
698 | | - for i in self.sender_input_indexes() { |
699 | | - log::trace!("Clearing sender input {i}"); |
700 | | - self.payjoin_psbt.inputs[i].non_witness_utxo = None; |
701 | | - self.payjoin_psbt.inputs[i].witness_utxo = None; |
702 | | - self.payjoin_psbt.inputs[i].final_script_sig = None; |
703 | | - self.payjoin_psbt.inputs[i].final_script_witness = None; |
704 | | - self.payjoin_psbt.inputs[i].tap_key_sig = None; |
| 712 | + |
| 713 | + for _ in &processed_psbt.outputs { |
| 714 | + filtered_psbt.outputs.push(bitcoin::psbt::Output::default()); |
705 | 715 | } |
706 | 716 |
|
707 | | - PayjoinProposal { payjoin_psbt: self.payjoin_psbt } |
| 717 | + log::trace!("Filtered PSBT: {filtered_psbt:#?}"); |
| 718 | + |
| 719 | + PayjoinProposal { payjoin_psbt: filtered_psbt } |
708 | 720 | } |
709 | 721 |
|
710 | 722 | /// Return the indexes of the sender inputs |
@@ -775,7 +787,10 @@ impl PayjoinProposal { |
775 | 787 | pub(crate) mod test { |
776 | 788 | use std::str::FromStr; |
777 | 789 |
|
778 | | - use bitcoin::{Address, Network}; |
| 790 | + use bitcoin::bip32::{DerivationPath, Fingerprint, Xpriv, Xpub}; |
| 791 | + use bitcoin::secp256k1::Secp256k1; |
| 792 | + use bitcoin::taproot::LeafVersion; |
| 793 | + use bitcoin::{Address, Network, ScriptBuf, TapLeafHash}; |
779 | 794 | use payjoin_test_utils::{PARSED_ORIGINAL_PSBT, QUERY_PARAMS, RECEIVER_INPUT_CONTRIBUTION}; |
780 | 795 | use rand::rngs::StdRng; |
781 | 796 | use rand::SeedableRng; |
@@ -1070,4 +1085,63 @@ pub(crate) mod test { |
1070 | 1085 | assert_eq!(original2, vec![1, 5, 4, 2, 6, 3]); |
1071 | 1086 | assert_eq!(original3, vec![4, 5, 1, 2, 6, 3]); |
1072 | 1087 | } |
| 1088 | + |
| 1089 | + /// Add keypath data to psbt to be prepared and verify it is excluded from the final PSBT |
| 1090 | + /// See: <https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#senders-payjoin-proposal-checklist> |
| 1091 | + #[test] |
| 1092 | + fn test_prepare_psbt_excludes_keypaths() { |
| 1093 | + let proposal = unchecked_proposal_from_test_vector(); |
| 1094 | + let mut processed_psbt = proposal.psbt.clone(); |
| 1095 | + |
| 1096 | + let secp = Secp256k1::new(); |
| 1097 | + let (_, pk) = secp.generate_keypair(&mut bitcoin::key::rand::thread_rng()); |
| 1098 | + let xpriv = Xpriv::new_master(Network::Bitcoin, &[]).expect("Could not generate new xpriv"); |
| 1099 | + let (x_only, _) = pk.x_only_public_key(); |
| 1100 | + |
| 1101 | + processed_psbt.xpub.insert( |
| 1102 | + Xpub::from_priv(&secp, &xpriv), |
| 1103 | + (Fingerprint::default(), DerivationPath::default()), |
| 1104 | + ); |
| 1105 | + |
| 1106 | + for input in &mut processed_psbt.inputs { |
| 1107 | + input.bip32_derivation.insert(pk, (Fingerprint::default(), DerivationPath::default())); |
| 1108 | + input.tap_key_origins.insert( |
| 1109 | + x_only, |
| 1110 | + ( |
| 1111 | + vec![TapLeafHash::from_script(&ScriptBuf::new(), LeafVersion::TapScript)], |
| 1112 | + (Fingerprint::default(), DerivationPath::default()), |
| 1113 | + ), |
| 1114 | + ); |
| 1115 | + input.tap_internal_key = Some(x_only); |
| 1116 | + } |
| 1117 | + |
| 1118 | + for output in &mut processed_psbt.outputs { |
| 1119 | + output.bip32_derivation.insert(pk, (Fingerprint::default(), DerivationPath::default())); |
| 1120 | + output.tap_key_origins.insert( |
| 1121 | + x_only, |
| 1122 | + ( |
| 1123 | + vec![TapLeafHash::from_script(&ScriptBuf::new(), LeafVersion::TapScript)], |
| 1124 | + (Fingerprint::default(), DerivationPath::default()), |
| 1125 | + ), |
| 1126 | + ); |
| 1127 | + output.tap_internal_key = Some(x_only); |
| 1128 | + } |
| 1129 | + |
| 1130 | + let provisional = provisional_proposal_from_test_vector(proposal); |
| 1131 | + let payjoin_proposal = provisional.prepare_psbt(processed_psbt); |
| 1132 | + |
| 1133 | + assert!(payjoin_proposal.payjoin_psbt.xpub.is_empty()); |
| 1134 | + |
| 1135 | + for input in &payjoin_proposal.payjoin_psbt.inputs { |
| 1136 | + assert!(input.bip32_derivation.is_empty()); |
| 1137 | + assert!(input.tap_key_origins.is_empty()); |
| 1138 | + assert!(input.tap_internal_key.is_none()); |
| 1139 | + } |
| 1140 | + |
| 1141 | + for output in &payjoin_proposal.payjoin_psbt.outputs { |
| 1142 | + assert!(output.bip32_derivation.is_empty()); |
| 1143 | + assert!(output.tap_key_origins.is_empty()); |
| 1144 | + assert!(output.tap_internal_key.is_none()); |
| 1145 | + } |
| 1146 | + } |
1073 | 1147 | } |
0 commit comments