1use bitcoin::psbt::{Psbt, PsbtSighashType};
20use bitcoin::sighash::TapSighashType;
21use bitcoin::{Amount, FeeRate, Script, ScriptBuf, TxOut, Weight};
22pub use error::{BuildSenderError, ResponseError, ValidationError, WellKnownError};
23pub(crate) use error::{InternalBuildSenderError, InternalProposalError, InternalValidationError};
24
25pub use crate::core::error_codes::ErrorCode;
26use crate::core::Url;
27use crate::output_substitution::OutputSubstitution;
28use crate::psbt::{AddressTypeError, PsbtExt, NON_WITNESS_INPUT_WEIGHT};
29use crate::Version;
30
31#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
33compile_error!("This crate currently only supports 32 bit and 64 bit architectures");
34
35mod error;
36
37#[cfg(feature = "v1")]
38#[cfg_attr(docsrs, doc(cfg(feature = "v1")))]
39pub mod v1;
40
41#[cfg(feature = "v2")]
42#[cfg_attr(docsrs, doc(cfg(feature = "v2")))]
43pub mod v2;
44
45type InternalResult<T> = Result<T, InternalProposalError>;
46
47#[derive(Clone)]
49pub(crate) struct PsbtContextBuilder {
50 pub(crate) psbt: Psbt,
51 pub(crate) payee: ScriptBuf,
52 pub(crate) amount: Option<bitcoin::Amount>,
53 pub(crate) fee_contribution: Option<(bitcoin::Amount, Option<usize>)>,
54 pub(crate) clamp_fee_contribution: bool,
60 pub(crate) min_fee_rate: FeeRate,
61}
62
63impl PsbtContextBuilder {
64 pub fn new(psbt: Psbt, payee: ScriptBuf, amount: Option<bitcoin::Amount>) -> Self {
69 Self {
70 psbt,
71 payee,
72 amount,
73 fee_contribution: None,
75 clamp_fee_contribution: false,
76 min_fee_rate: FeeRate::ZERO,
77 }
78 }
79
80 pub fn build_recommended(
87 self,
88 min_fee_rate: FeeRate,
89 output_substitution: OutputSubstitution,
90 ) -> Result<PsbtContext, BuildSenderError> {
91 let mut payout_scripts = std::iter::once(self.payee.clone());
94
95 if self.psbt.unsigned_tx.output.len() == 1
97 && payout_scripts.all(|script| script == self.psbt.unsigned_tx.output[0].script_pubkey)
98 {
99 return self.build_non_incentivizing(min_fee_rate, output_substitution);
100 }
101
102 if let Some((additional_fee_index, fee_available)) = self
103 .psbt
104 .unsigned_tx
105 .output
106 .clone()
107 .into_iter()
108 .enumerate()
109 .find(|(_, txo)| payout_scripts.all(|script| script != txo.script_pubkey))
110 .map(|(i, txo)| (i, txo.value))
111 {
112 let mut input_pairs = self.psbt.input_pairs();
113 let first_input_pair = input_pairs.next().ok_or(InternalBuildSenderError::NoInputs)?;
114 let mut input_weight = first_input_pair
115 .expected_input_weight()
116 .map_err(InternalBuildSenderError::InputWeight)?;
117 for input_pair in input_pairs {
118 if input_pair.address_type()? != first_input_pair.address_type()? {
120 input_weight =
121 bitcoin::transaction::InputWeightPrediction::P2TR_KEY_NON_DEFAULT_SIGHASH
122 .weight()
123 + NON_WITNESS_INPUT_WEIGHT;
124 break;
125 }
126 }
127
128 let recommended_additional_fee = min_fee_rate
129 .checked_mul_by_weight(input_weight)
130 .ok_or(InternalBuildSenderError::AddressType(AddressTypeError::FeeRateOverflow))?;
131 if fee_available < recommended_additional_fee {
132 tracing::warn!("Insufficient funds to maintain specified minimum feerate.");
133 return self.build_with_additional_fee(
134 fee_available,
135 Some(additional_fee_index),
136 min_fee_rate,
137 true,
138 output_substitution,
139 );
140 }
141 return self.build_with_additional_fee(
142 recommended_additional_fee,
143 Some(additional_fee_index),
144 min_fee_rate,
145 false,
146 output_substitution,
147 );
148 }
149 self.build_non_incentivizing(min_fee_rate, output_substitution)
150 }
151
152 pub fn build_with_additional_fee(
166 mut self,
167 max_fee_contribution: bitcoin::Amount,
168 change_index: Option<usize>,
169 min_fee_rate: FeeRate,
170 clamp_fee_contribution: bool,
171 output_substitution: OutputSubstitution,
172 ) -> Result<PsbtContext, BuildSenderError> {
173 self.fee_contribution = Some((max_fee_contribution, change_index));
174 self.clamp_fee_contribution = clamp_fee_contribution;
175 self.min_fee_rate = min_fee_rate;
176 self.build(output_substitution)
177 }
178
179 pub fn build_non_incentivizing(
184 mut self,
185 min_fee_rate: FeeRate,
186 output_substitution: OutputSubstitution,
187 ) -> Result<PsbtContext, BuildSenderError> {
188 self.fee_contribution = None;
191 self.clamp_fee_contribution = false;
192 self.min_fee_rate = min_fee_rate;
193 self.build(output_substitution)
194 }
195
196 fn build(
197 self,
198 output_substitution: OutputSubstitution,
199 ) -> Result<PsbtContext, BuildSenderError> {
200 let psbt =
201 self.psbt.validate().map_err(InternalBuildSenderError::InconsistentOriginalPsbt)?;
202 psbt.validate_input_utxos().map_err(InternalBuildSenderError::InvalidOriginalInput)?;
203
204 for input in &psbt.inputs {
214 if let Some(sighash_type) = input.sighash_type {
215 ensure(
216 commits_to_all_inputs_and_outputs(sighash_type),
217 InternalBuildSenderError::OriginalTxinNonAllSighashType,
218 )?;
219 }
220 }
221
222 check_single_payee(&psbt, &self.payee, self.amount)?;
223 let fee_contribution = determine_fee_contribution(
224 &psbt,
225 &self.payee,
226 self.fee_contribution,
227 self.clamp_fee_contribution,
228 )?;
229
230 Ok(PsbtContext {
231 original_psbt: psbt,
232 output_substitution,
233 fee_contribution,
234 min_fee_rate: self.min_fee_rate,
235 payee: self.payee,
236 })
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241#[cfg_attr(feature = "v2", derive(serde::Serialize, serde::Deserialize))]
242pub(crate) struct AdditionalFeeContribution {
243 max_amount: Amount,
244 vout: usize,
245}
246
247#[derive(Debug, Clone)]
249#[cfg_attr(feature = "v2", derive(serde::Serialize, serde::Deserialize, PartialEq, Eq))]
250pub struct PsbtContext {
251 original_psbt: Psbt,
252 output_substitution: OutputSubstitution,
253 fee_contribution: Option<AdditionalFeeContribution>,
254 min_fee_rate: FeeRate,
255 payee: ScriptBuf,
256}
257
258macro_rules! check_eq {
259 ($proposed:expr, $original:expr, $error:ident) => {
260 match ($proposed, $original) {
261 (proposed, original) if proposed != original =>
262 return Err(InternalProposalError::$error { proposed, original }),
263 _ => (),
264 }
265 };
266}
267
268fn ensure<T>(condition: bool, error: T) -> Result<(), T> {
269 if !condition {
270 return Err(error);
271 }
272 Ok(())
273}
274
275fn commits_to_all_inputs_and_outputs(sighash_type: PsbtSighashType) -> bool {
283 matches!(sighash_type.taproot_hash_ty(), Ok(TapSighashType::Default | TapSighashType::All))
286}
287
288impl PsbtContext {
289 fn process_proposal(self, mut proposal: Psbt) -> InternalResult<Psbt> {
290 self.basic_checks(&proposal)?;
291 self.check_inputs(&proposal, true)?;
292 let contributed_fee = self.check_outputs(&proposal)?;
293 self.restore_original_utxos(&mut proposal)?;
294 self.restore_original_outputs(&mut proposal)?;
295 self.check_fees(&proposal, contributed_fee)?;
296 Ok(proposal)
297 }
298
299 fn check_fees(&self, proposal: &Psbt, contributed_fee: Amount) -> InternalResult<()> {
300 let proposed_fee = proposal.fee().map_err(InternalProposalError::Psbt)?;
301 let original_fee = self.original_psbt.fee().map_err(InternalProposalError::Psbt)?;
302 ensure(original_fee <= proposed_fee, InternalProposalError::AbsoluteFeeDecreased)?;
303 ensure(
304 contributed_fee <= proposed_fee - original_fee,
305 InternalProposalError::PayeeTookContributedFee,
306 )?;
307 let original_weight = self.original_psbt.clone().extract_tx_unchecked_fee_rate().weight();
308 let original_fee_rate = original_fee / original_weight;
309 let original_spks = self
310 .original_psbt
311 .input_pairs()
312 .map(|input_pair| {
313 input_pair
314 .previous_txout()
315 .map_err(InternalProposalError::PrevTxOut)
316 .map(|txout| txout.script_pubkey.clone())
317 })
318 .collect::<InternalResult<Vec<ScriptBuf>>>()?;
319 let additional_input_weight = proposal.input_pairs().try_fold(
320 Weight::ZERO,
321 |acc, input_pair| -> InternalResult<Weight> {
322 let spk = &input_pair
323 .previous_txout()
324 .map_err(InternalProposalError::PrevTxOut)?
325 .script_pubkey;
326 if original_spks.contains(spk) {
327 Ok(acc)
328 } else {
329 let weight = input_pair
330 .expected_input_weight()
331 .map_err(InternalProposalError::InputWeight)?;
332 Ok(acc + weight)
333 }
334 },
335 )?;
336 ensure(
337 contributed_fee <= original_fee_rate * additional_input_weight,
338 InternalProposalError::FeeContributionPaysOutputSizeIncrease,
339 )?;
340 if self.min_fee_rate > FeeRate::ZERO {
341 let proposed_weight = proposal.clone().extract_tx_unchecked_fee_rate().weight();
342 ensure(
343 proposed_fee / proposed_weight >= self.min_fee_rate,
344 InternalProposalError::FeeRateBelowMinimum,
345 )?;
346 }
347 Ok(())
348 }
349
350 fn basic_checks(&self, proposal: &Psbt) -> InternalResult<()> {
352 check_eq!(
353 proposal.unsigned_tx.version,
354 self.original_psbt.unsigned_tx.version,
355 VersionsDontMatch
356 );
357 check_eq!(
358 proposal.unsigned_tx.lock_time,
359 self.original_psbt.unsigned_tx.lock_time,
360 LockTimesDontMatch
361 );
362 Ok(())
363 }
364
365 fn check_inputs(
366 &self,
367 proposal: &Psbt,
368 ensure_receiver_input_finalized: bool,
369 ) -> InternalResult<()> {
370 let mut original_inputs = self.original_psbt.input_pairs().peekable();
371
372 for proposed in proposal.input_pairs() {
373 ensure(
374 proposed.psbtin.bip32_derivation.is_empty(),
375 InternalProposalError::TxInContainsKeyPaths,
376 )?;
377 ensure(
378 proposed.psbtin.partial_sigs.is_empty(),
379 InternalProposalError::ContainsPartialSigs,
380 )?;
381 match original_inputs.peek() {
382 Some(original)
384 if proposed.txin.previous_output == original.txin.previous_output =>
385 {
386 check_eq!(
387 proposed.txin.sequence,
388 original.txin.sequence,
389 SenderTxinSequenceChanged
390 );
391 ensure(
392 proposed.psbtin.final_script_sig.is_none(),
393 InternalProposalError::SenderTxinContainsFinalScriptSig,
394 )?;
395 ensure(
396 proposed.psbtin.final_script_witness.is_none(),
397 InternalProposalError::SenderTxinContainsFinalScriptWitness,
398 )?;
399 if let Some(sighash_type) = proposed.psbtin.sighash_type {
402 ensure(
403 commits_to_all_inputs_and_outputs(sighash_type),
404 InternalProposalError::SenderTxinNonAllSighashType,
405 )?;
406 }
407 original_inputs.next();
408 }
409 None | Some(_) => {
411 let original = self
412 .original_psbt
413 .input_pairs()
414 .next()
415 .ok_or(InternalProposalError::NoInputs)?;
416 if ensure_receiver_input_finalized {
417 ensure(
419 proposed.psbtin.final_script_sig.is_some()
420 || proposed.psbtin.final_script_witness.is_some(),
421 InternalProposalError::ReceiverTxinNotFinalized,
422 )?;
423 }
424 ensure(
426 proposed.psbtin.witness_utxo.is_some()
427 || proposed.psbtin.non_witness_utxo.is_some(),
428 InternalProposalError::ReceiverTxinMissingUtxoInfo,
429 )?;
430 ensure(
431 proposed.txin.sequence == original.txin.sequence,
432 InternalProposalError::MixedSequence,
433 )?;
434 }
435 }
436 }
437 ensure(original_inputs.peek().is_none(), InternalProposalError::MissingOrShuffledInputs)?;
438 Ok(())
439 }
440
441 fn restore_original_utxos(&self, proposal: &mut Psbt) -> InternalResult<()> {
445 let mut original_inputs = self.original_psbt.input_pairs().peekable();
446 let proposal_inputs =
447 proposal.unsigned_tx.input.iter().zip(&mut proposal.inputs).peekable();
448
449 for (proposed_txin, proposed_psbtin) in proposal_inputs {
450 if let Some(original) = original_inputs.peek() {
451 if proposed_txin.previous_output == original.txin.previous_output {
452 proposed_psbtin.non_witness_utxo = original.psbtin.non_witness_utxo.clone();
453 proposed_psbtin.witness_utxo = original.psbtin.witness_utxo.clone();
454 proposed_psbtin.bip32_derivation = original.psbtin.bip32_derivation.clone();
455 proposed_psbtin.tap_internal_key = original.psbtin.tap_internal_key;
456 proposed_psbtin.tap_key_origins = original.psbtin.tap_key_origins.clone();
457 proposed_psbtin.witness_script = original.psbtin.witness_script.clone();
458 original_inputs.next();
459 }
460 }
461 }
462 Ok(())
463 }
464
465 fn restore_original_outputs(&self, proposal: &mut Psbt) -> InternalResult<()> {
469 let mut original_outputs = self
470 .original_psbt
471 .unsigned_tx
472 .output
473 .iter()
474 .zip(self.original_psbt.outputs.iter())
475 .peekable();
476 let proposal_outputs = proposal.unsigned_tx.output.iter().zip(proposal.outputs.iter_mut());
477
478 for (proposed_txout, proposed_psbtout) in proposal_outputs {
479 if let Some((original_txout, original_psbtout)) = original_outputs.peek() {
480 if proposed_txout == *original_txout {
481 *proposed_psbtout = (*original_psbtout).clone();
482 original_outputs.next();
483 }
484 }
485 }
486
487 Ok(())
488 }
489
490 fn check_outputs(&self, proposal: &Psbt) -> InternalResult<Amount> {
491 let mut original_outputs =
492 self.original_psbt.unsigned_tx.output.iter().enumerate().peekable();
493 let mut contributed_fee = Amount::ZERO;
494
495 for (proposed_txout, proposed_psbtout) in
496 proposal.unsigned_tx.output.iter().zip(&proposal.outputs)
497 {
498 ensure(
499 proposed_psbtout.bip32_derivation.is_empty(),
500 InternalProposalError::TxOutContainsKeyPaths,
501 )?;
502 match (original_outputs.peek(), self.fee_contribution) {
503 (
505 Some((original_output_index, original_output)),
506 Some(AdditionalFeeContribution {
507 max_amount: max_fee_contrib,
508 vout: fee_contrib_idx,
509 }),
510 ) if proposed_txout.script_pubkey == original_output.script_pubkey
511 && *original_output_index == fee_contrib_idx =>
512 {
513 if proposed_txout.value < original_output.value {
514 contributed_fee = original_output.value - proposed_txout.value;
515 ensure(
516 contributed_fee <= max_fee_contrib,
517 InternalProposalError::FeeContributionExceedsMaximum,
518 )?;
519 }
521 original_outputs.next();
522 }
523 (Some((_original_output_index, original_output)), _)
525 if original_output.script_pubkey == self.payee =>
526 {
527 ensure(
528 self.output_substitution == OutputSubstitution::Enabled
529 || (proposed_txout.script_pubkey == original_output.script_pubkey
530 && proposed_txout.value >= original_output.value),
531 InternalProposalError::DisallowedOutputSubstitution,
532 )?;
533 original_outputs.next();
534 }
535 (Some((_original_output_index, original_output)), _)
537 if proposed_txout.script_pubkey == original_output.script_pubkey =>
538 {
539 ensure(
540 proposed_txout.value >= original_output.value,
541 InternalProposalError::OutputValueDecreased,
542 )?;
543 original_outputs.next();
544 }
545 _ => (),
547 }
548 }
549
550 ensure(original_outputs.peek().is_none(), InternalProposalError::MissingOrShuffledOutputs)?;
551 Ok(contributed_fee)
552 }
553}
554
555fn check_single_payee(
558 psbt: &Psbt,
559 script_pubkey: &Script,
560 amount: Option<bitcoin::Amount>,
561) -> Result<(), InternalBuildSenderError> {
562 let mut payee_found = false;
563 for output in &psbt.unsigned_tx.output {
564 if output.script_pubkey == *script_pubkey {
565 if let Some(amount) = amount {
566 if output.value != amount {
567 return Err(InternalBuildSenderError::PayeeValueNotEqual);
568 }
569 }
570 if payee_found {
571 return Err(InternalBuildSenderError::MultiplePayeeOutputs);
572 }
573 payee_found = true;
574 }
575 }
576 if payee_found {
577 Ok(())
578 } else {
579 Err(InternalBuildSenderError::MissingPayeeOutput)
580 }
581}
582
583fn clear_unneeded_fields(psbt: &mut Psbt) {
584 psbt.xpub_mut().clear();
585 psbt.proprietary_mut().clear();
586 psbt.unknown_mut().clear();
587 for input in psbt.inputs_mut() {
588 input.bip32_derivation.clear();
589 input.tap_internal_key = None;
590 input.tap_key_origins.clear();
591 input.tap_key_sig = None;
592 input.tap_merkle_root = None;
593 input.tap_script_sigs.clear();
594 input.proprietary.clear();
595 input.unknown.clear();
596 }
597 for output in psbt.outputs_mut() {
598 output.bip32_derivation.clear();
599 output.tap_internal_key = None;
600 output.tap_key_origins.clear();
601 output.proprietary.clear();
602 output.unknown.clear();
603 }
604}
605
606fn check_fee_output_amount(
608 output: &TxOut,
609 fee: bitcoin::Amount,
610 clamp_fee_contribution: bool,
611) -> Result<bitcoin::Amount, InternalBuildSenderError> {
612 if output.value < fee {
613 if clamp_fee_contribution {
614 Ok(output.value)
615 } else {
616 Err(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution)
617 }
618 } else {
619 Ok(fee)
620 }
621}
622
623fn find_change_index(
625 psbt: &Psbt,
626 payee: &Script,
627 fee: bitcoin::Amount,
628 clamp_fee_contribution: bool,
629) -> Result<Option<AdditionalFeeContribution>, InternalBuildSenderError> {
630 match (psbt.unsigned_tx.output.len(), clamp_fee_contribution) {
631 (0, _) => return Err(InternalBuildSenderError::NoOutputs),
632 (1, false) if psbt.unsigned_tx.output[0].script_pubkey == *payee =>
633 return Err(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution),
634 (1, true) if psbt.unsigned_tx.output[0].script_pubkey == *payee => return Ok(None),
635 (1, _) => return Err(InternalBuildSenderError::MissingPayeeOutput),
636 (2, _) => (),
637 _ => return Err(InternalBuildSenderError::AmbiguousChangeOutput),
638 }
639 let (index, output) = psbt
640 .unsigned_tx
641 .output
642 .iter()
643 .enumerate()
644 .find(|(_, output)| output.script_pubkey != *payee)
645 .ok_or(InternalBuildSenderError::MultiplePayeeOutputs)?;
646
647 Ok(Some(AdditionalFeeContribution {
648 max_amount: check_fee_output_amount(output, fee, clamp_fee_contribution)?,
649 vout: index,
650 }))
651}
652
653fn check_change_index(
656 psbt: &Psbt,
657 payee: &Script,
658 fee: bitcoin::Amount,
659 index: usize,
660 clamp_fee_contribution: bool,
661) -> Result<AdditionalFeeContribution, InternalBuildSenderError> {
662 let output = psbt
663 .unsigned_tx
664 .output
665 .get(index)
666 .ok_or(InternalBuildSenderError::ChangeIndexOutOfBounds)?;
667 if output.script_pubkey == *payee {
668 return Err(InternalBuildSenderError::ChangeIndexPointsAtPayee);
669 }
670 Ok(AdditionalFeeContribution {
671 max_amount: check_fee_output_amount(output, fee, clamp_fee_contribution)?,
672 vout: index,
673 })
674}
675
676fn determine_fee_contribution(
677 psbt: &Psbt,
678 payee: &Script,
679 fee_contribution: Option<(bitcoin::Amount, Option<usize>)>,
680 clamp_fee_contribution: bool,
681) -> Result<Option<AdditionalFeeContribution>, InternalBuildSenderError> {
682 Ok(match fee_contribution {
683 Some((fee, None)) => find_change_index(psbt, payee, fee, clamp_fee_contribution)?,
684 Some((fee, Some(index))) =>
685 Some(check_change_index(psbt, payee, fee, index, clamp_fee_contribution)?),
686 None => None,
687 })
688}
689
690fn serialize_url(
691 endpoint: Url,
692 output_substitution: OutputSubstitution,
693 fee_contribution: Option<AdditionalFeeContribution>,
694 min_fee_rate: FeeRate,
695 version: Version,
696) -> Url {
697 let mut url = endpoint;
698 url.query_pairs_mut().append_pair("v", &version.to_string());
699 if output_substitution == OutputSubstitution::Disabled {
700 url.query_pairs_mut().append_pair("disableoutputsubstitution", "true");
701 }
702 if let Some(AdditionalFeeContribution { max_amount, vout }) = fee_contribution {
703 url.query_pairs_mut()
704 .append_pair("additionalfeeoutputindex", &vout.to_string())
705 .append_pair("maxadditionalfeecontribution", &max_amount.to_sat().to_string());
706 }
707 if min_fee_rate > FeeRate::ZERO {
708 let float_fee_rate = min_fee_rate.to_sat_per_kwu() as f32 / 250.0_f32;
710 url.query_pairs_mut().append_pair("minfeerate", &float_fee_rate.to_string());
711 }
712 url
713}
714
715#[cfg(test)]
716mod test {
717 use bitcoin::absolute::LockTime;
718 use bitcoin::bip32::{DerivationPath, Fingerprint};
719 use bitcoin::ecdsa::Signature;
720 use bitcoin::hex::FromHex;
721 use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, SecretKey, SECP256K1};
722 use bitcoin::taproot::TaprootBuilder;
723 use bitcoin::{Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, Witness};
724 use payjoin_test_utils::{
725 BoxError, ADDITIONAL_FEE_OUTPUT_INDEX, MAX_ADDITIONAL_FEE_CONTRIBUTION,
726 PARSED_ORIGINAL_PSBT, PARSED_PAYJOIN_PROPOSAL, PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO,
727 };
728
729 use super::*;
730 use crate::core::Url;
731 use crate::output_substitution::OutputSubstitution;
732 use crate::psbt::PsbtExt;
733 use crate::send::{AdditionalFeeContribution, InternalBuildSenderError, InternalProposalError};
734
735 pub(crate) fn create_psbt_context() -> Result<super::PsbtContext, BoxError> {
737 let payee = PARSED_ORIGINAL_PSBT.unsigned_tx.output[1].script_pubkey.clone();
738 Ok(super::PsbtContext {
739 original_psbt: PARSED_ORIGINAL_PSBT.clone(),
740 output_substitution: OutputSubstitution::Enabled,
741 fee_contribution: Some(AdditionalFeeContribution {
742 max_amount: bitcoin::Amount::from_sat(182),
743 vout: 0,
744 }),
745 min_fee_rate: FeeRate::ZERO,
746 payee,
747 })
748 }
749
750 #[test]
751 fn test_restore_original_utxos() -> Result<(), BoxError> {
752 let mut original_psbt = PARSED_ORIGINAL_PSBT.clone();
753 let mut payjoin_proposal = PARSED_PAYJOIN_PROPOSAL.clone();
754 let payee = original_psbt.unsigned_tx.output[1].script_pubkey.clone();
755 let (_, pk) = SECP256K1.generate_keypair(&mut bitcoin::key::rand::thread_rng());
756 let x_only = pk.x_only_public_key().0;
757 let _ = original_psbt.inputs[0].tap_internal_key.insert(x_only);
759 let _ = original_psbt.outputs[0].tap_internal_key.insert(x_only);
760 original_psbt.inputs[0]
761 .bip32_derivation
762 .insert(pk, (Fingerprint::default(), DerivationPath::default()));
763 original_psbt.inputs[0]
764 .tap_key_origins
765 .insert(x_only, (vec![], (Fingerprint::default(), DerivationPath::default())));
766 original_psbt.inputs[0].witness_script = Some(payee.clone());
767 let prev_txout = TxOut { value: Amount::ONE_BTC, script_pubkey: payee.clone() };
768 original_psbt.inputs[0].witness_utxo = Some(prev_txout.clone());
769 let psbt_ctx = PsbtContextBuilder::new(original_psbt, payee.clone(), None)
770 .build(OutputSubstitution::Disabled)?;
771 clear_unneeded_fields(&mut payjoin_proposal);
772
773 psbt_ctx.restore_original_utxos(&mut payjoin_proposal)?;
774 assert!(payjoin_proposal.inputs[0].bip32_derivation.contains_key(&pk));
775 assert!(payjoin_proposal.inputs[0].tap_key_origins.contains_key(&x_only));
776 assert_eq!(payjoin_proposal.inputs[0].witness_utxo, Some(prev_txout));
777 assert_eq!(payjoin_proposal.inputs[0].tap_internal_key, Some(x_only));
778 assert_eq!(payjoin_proposal.inputs[0].witness_script, Some(payee));
779 Ok(())
780 }
781
782 #[test]
783 fn test_restore_original_outputs() -> Result<(), BoxError> {
784 let mut original_psbt = PARSED_ORIGINAL_PSBT.clone();
785 let payee = original_psbt.unsigned_tx.output[1].script_pubkey.clone();
786 let (_, pk) = SECP256K1.generate_keypair(&mut bitcoin::key::rand::thread_rng());
787 let x_only = pk.x_only_public_key().0;
788 let taptree = TaprootBuilder::new()
789 .add_leaf(0, payee.clone())?
790 .try_into_taptree()
791 .expect("Valid tap tree");
792
793 original_psbt.outputs[0].witness_script = Some(payee.clone());
794 original_psbt.outputs[0]
795 .bip32_derivation
796 .insert(pk, (Fingerprint::default(), DerivationPath::default()));
797 original_psbt.outputs[0].tap_internal_key = Some(x_only);
798 original_psbt.outputs[0]
799 .tap_key_origins
800 .insert(x_only, (vec![], (Fingerprint::default(), DerivationPath::default())));
801 original_psbt.outputs[0].tap_tree = Some(taptree.clone());
802
803 let psbt_ctx = PsbtContextBuilder::new(original_psbt.clone(), payee.clone(), None)
804 .build(OutputSubstitution::Disabled)?;
805
806 let mut payjoin_proposal = original_psbt.clone();
807 clear_unneeded_fields(&mut payjoin_proposal);
808 psbt_ctx.restore_original_outputs(&mut payjoin_proposal)?;
809 assert_eq!(payjoin_proposal.outputs[0].witness_script, Some(payee));
810 assert!(payjoin_proposal.outputs[0].bip32_derivation.contains_key(&pk));
811 assert!(payjoin_proposal.outputs[0].tap_key_origins.contains_key(&x_only));
812 assert_eq!(payjoin_proposal.outputs[0].tap_internal_key, Some(x_only));
813 assert_eq!(payjoin_proposal.outputs[0].tap_tree, Some(taptree));
814 Ok(())
815 }
816
817 #[test]
818 fn test_determine_fees() -> Result<(), BoxError> {
819 let fee_contribution = determine_fee_contribution(
820 &PARSED_ORIGINAL_PSBT,
821 Script::from_bytes(&<Vec<u8> as FromHex>::from_hex(
822 "0014b60943f60c3ee848828bdace7474a92e81f3fcdd",
823 )?),
824 Some((Amount::from_sat(1000), Some(1))),
825 false,
826 );
827 assert_eq!((*fee_contribution.as_ref().expect("Failed to retrieve fees")).unwrap().vout, 1);
828 assert_eq!(
829 (*fee_contribution.as_ref().expect("Failed to retrieve fees")).unwrap().max_amount,
830 Amount::from_sat(1000)
831 );
832 Ok(())
833 }
834
835 #[test]
836 fn test_insufficient_fees() -> Result<(), BoxError> {
837 let fee_contribution = determine_fee_contribution(
838 &PARSED_ORIGINAL_PSBT,
839 Script::from_bytes(&<Vec<u8> as FromHex>::from_hex(
840 "0014b60943f60c3ee848828bdace7474a92e81f3fcdd",
841 )?),
842 Some((Amount::from_sat(100000000), None)),
843 false,
844 );
845 assert_eq!(
846 fee_contribution.err(),
847 Some(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution)
848 );
849 let fee_contribution = determine_fee_contribution(
851 &PARSED_ORIGINAL_PSBT,
852 Script::from_bytes(&<Vec<u8> as FromHex>::from_hex(
853 "0014b60943f60c3ee848828bdace7474a92e81f3fcdd",
854 )?),
855 Some((Amount::from_sat(95983068), None)),
856 false,
857 );
858 assert!(fee_contribution.is_ok());
859 Ok(())
860 }
861
862 #[test]
863 fn test_self_pay_change_index() -> Result<(), BoxError> {
864 let script_bytes =
865 <Vec<u8> as FromHex>::from_hex("a914774096dbcf486743c22f4347e9b469febe8b677a87")?;
866 let payee_script = Script::from_bytes(&script_bytes);
867 let fee_contribution = determine_fee_contribution(
868 &PARSED_ORIGINAL_PSBT,
869 payee_script,
870 Some((Amount::from_sat(1000), Some(1))),
871 false,
872 );
873 assert_eq!(
874 *payee_script,
875 PARSED_ORIGINAL_PSBT
876 .unsigned_tx
877 .output
878 .get(1)
879 .ok_or(InternalBuildSenderError::ChangeIndexOutOfBounds)
880 .unwrap()
881 .script_pubkey
882 );
883 assert!(fee_contribution.as_ref().is_err(), "determine fee contribution expected Change output points at payee error, but it succeeded");
884 match fee_contribution.as_ref() {
885 Ok(_) => panic!("Expected error, got success"),
886 Err(error) => {
887 assert_eq!(*error, InternalBuildSenderError::ChangeIndexPointsAtPayee);
888 }
889 }
890 Ok(())
891 }
892
893 #[test]
894 fn test_find_change_index() -> Result<(), BoxError> {
895 let mut psbt = PARSED_ORIGINAL_PSBT.clone();
898 let payee_script = ScriptBuf::from_hex("0014b60943f60c3ee848828bdace7474a92e81f3fcdd")?;
899 let fee_contribution = determine_fee_contribution(
900 &psbt,
901 &payee_script,
902 Some((Amount::from_sat(1000), None)),
903 true,
904 );
905 assert!(
906 fee_contribution.as_ref().is_ok(),
907 "Expected an Ok result got: {:#?}",
908 fee_contribution.as_ref().err()
909 );
910 assert_eq!((*fee_contribution.as_ref().expect("Failed to retrieve fees")).unwrap().vout, 0);
911 assert_eq!(
912 (*fee_contribution.as_ref().expect("Failed to retrieve fees")).unwrap().max_amount,
913 Amount::from_sat(1000)
914 );
915
916 psbt.outputs.clear();
918 psbt.unsigned_tx.output.clear();
919
920 let fee_contribution = determine_fee_contribution(
921 &psbt,
922 &ScriptBuf::from_hex("0014908eb2d695cf78e39a621d1561655790d1a8c60f")?,
923 Some((Amount::from_sat(1000), None)),
924 true,
925 );
926 assert_eq!(fee_contribution, Err(InternalBuildSenderError::NoOutputs));
927
928 let mut psbt = PARSED_ORIGINAL_PSBT.clone();
930 psbt.outputs[1] = psbt.outputs[0].clone();
931 psbt.unsigned_tx.output[1].script_pubkey = psbt.unsigned_tx.output[0].script_pubkey.clone();
932
933 let fee_contribution = determine_fee_contribution(
934 &psbt,
935 &ScriptBuf::from_hex("a9141de849f069d274150e3afeae8d72eb5a6b09443087")?,
936 Some((Amount::from_sat(1000), None)),
937 true,
938 );
939 assert_eq!(fee_contribution, Err(InternalBuildSenderError::MultiplePayeeOutputs));
940
941 let mut psbt = PARSED_ORIGINAL_PSBT.clone();
943 psbt.outputs.pop();
944 psbt.unsigned_tx.output.pop();
945
946 let fee_contribution = determine_fee_contribution(
947 &psbt,
948 Script::from_bytes(
949 &<Vec<u8> as FromHex>::from_hex("a9141de849f069d274150e3afeae8d72eb5a6b09443087")
950 .unwrap(),
951 ),
952 Some((Amount::from_sat(1000), None)),
953 true,
954 );
955 assert_eq!(fee_contribution, Ok(None));
956
957 let fee_contribution = determine_fee_contribution(
958 &psbt,
959 Script::from_bytes(
960 &<Vec<u8> as FromHex>::from_hex("a9141de849f069d274150e3afeae8d72eb5a6b09443087")
961 .unwrap(),
962 ),
963 Some((Amount::from_sat(1000), None)),
964 false,
965 );
966 assert_eq!(
967 fee_contribution,
968 Err(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution)
969 );
970
971 let fee_contribution = determine_fee_contribution(
972 &psbt,
973 &payee_script,
974 Some((Amount::from_sat(1000), None)),
975 false,
976 );
977 assert_eq!(fee_contribution, Err(InternalBuildSenderError::MissingPayeeOutput));
978
979 let fee_contribution = determine_fee_contribution(
980 &psbt,
981 &payee_script,
982 Some((Amount::from_sat(1000), None)),
983 true,
984 );
985 assert_eq!(fee_contribution, Err(InternalBuildSenderError::MissingPayeeOutput));
986
987 let mut psbt = PARSED_ORIGINAL_PSBT.clone();
989 psbt.outputs.push(psbt.outputs[1].clone());
990 psbt.unsigned_tx.output.push(psbt.unsigned_tx.output[1].clone());
991
992 let fee_contribution = determine_fee_contribution(
993 &psbt,
994 &payee_script,
995 Some((Amount::from_sat(1000), None)),
996 true,
997 );
998 assert_eq!(fee_contribution, Err(InternalBuildSenderError::AmbiguousChangeOutput));
999 Ok(())
1000 }
1001
1002 #[test]
1003 fn test_single_payee_amount_mismatch() -> Result<(), BoxError> {
1004 let payee_script = ScriptBuf::from_hex("a914774096dbcf486743c22f4347e9b469febe8b677a87")?;
1005 let single_payee =
1006 check_single_payee(&PARSED_ORIGINAL_PSBT, &payee_script, Some(Amount::from_sat(1)));
1007 assert!(
1008 PARSED_ORIGINAL_PSBT
1009 .unsigned_tx
1010 .output
1011 .get(1)
1012 .ok_or(InternalBuildSenderError::ChangeIndexOutOfBounds)
1013 .unwrap()
1014 .script_pubkey
1015 == payee_script
1016 );
1017 assert!(
1018 single_payee.is_err(),
1019 "Check single payee expected payee value not equal error, but it succeeded"
1020 );
1021 match single_payee {
1022 Ok(_) => panic!("Expected error, got success"),
1023 Err(error) => {
1024 assert_eq!(error, InternalBuildSenderError::PayeeValueNotEqual);
1025 }
1026 }
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn test_equal_amount_fee_contribution() -> Result<(), BoxError> {
1032 let mut ctx = create_psbt_context()?;
1033 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1034
1035 ctx.fee_contribution = None;
1036 proposal.unsigned_tx.output[0].value = ctx.original_psbt.unsigned_tx.output[0].value;
1037
1038 assert!(ctx.process_proposal(proposal).is_ok());
1039
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn test_payee_output_value_decreased() -> Result<(), BoxError> {
1045 let mut ctx = create_psbt_context()?;
1046 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1047
1048 ctx.fee_contribution = None;
1049 proposal.unsigned_tx.output[0].value =
1050 ctx.original_psbt.unsigned_tx.output[0].value - Amount::from_sat(1);
1051
1052 ctx.original_psbt.unsigned_tx.output[0].script_pubkey =
1053 ctx.original_psbt.unsigned_tx.output[1].script_pubkey.clone();
1054 assert!(ctx.clone().process_proposal(proposal.clone()).is_ok());
1055
1056 ctx.original_psbt.unsigned_tx.output[0].script_pubkey = ctx.payee.clone();
1057 assert!(ctx.process_proposal(proposal).is_ok());
1058
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn test_official_vectors() -> Result<(), BoxError> {
1064 let ctx = create_psbt_context()?;
1065 let mut proposal = PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO.clone();
1066 for output in proposal.outputs_mut() {
1067 output.bip32_derivation.clear();
1068 }
1069 for input in proposal.inputs_mut() {
1070 input.bip32_derivation.clear();
1071 }
1072 proposal.inputs_mut()[0].witness_utxo = None;
1073 let result = ctx.process_proposal(proposal);
1074 assert!(result.is_ok(), "Expected an Ok result got: {:#?}", result.err());
1075 assert_eq!(
1076 result.unwrap().inputs_mut()[0].witness_utxo,
1077 PARSED_ORIGINAL_PSBT.inputs[0].witness_utxo,
1078 );
1079 Ok(())
1080 }
1081
1082 #[test]
1083 fn test_disable_output_substitution_query_param() -> Result<(), BoxError> {
1084 let url = serialize_url(
1085 Url::parse("http://localhost")?,
1086 OutputSubstitution::Disabled,
1087 None,
1088 FeeRate::ZERO,
1089 Version::Two,
1090 );
1091 assert_eq!(url, Url::parse("http://localhost?v=2&disableoutputsubstitution=true")?);
1092
1093 let url = serialize_url(
1094 Url::parse("http://localhost")?,
1095 OutputSubstitution::Enabled,
1096 None,
1097 FeeRate::ZERO,
1098 Version::Two,
1099 );
1100 assert_eq!(url, Url::parse("http://localhost?v=2")?);
1101 Ok(())
1102 }
1103
1104 #[test]
1105 fn test_min_feerate_query_param() -> Result<(), BoxError> {
1106 let url = serialize_url(
1107 Url::parse("http://localhost")?,
1108 OutputSubstitution::Enabled,
1109 None,
1110 FeeRate::from_sat_per_vb(10).expect("Could not parse feerate"),
1111 Version::Two,
1112 );
1113 assert_eq!(url, Url::parse("http://localhost?v=2&minfeerate=10")?);
1114 Ok(())
1115 }
1116
1117 #[test]
1118 fn test_additional_fee_contribution_query_param() -> Result<(), BoxError> {
1119 let url = serialize_url(
1120 Url::parse("http://localhost")?,
1121 OutputSubstitution::Enabled,
1122 Some(AdditionalFeeContribution { max_amount: Amount::from_sat(1000), vout: 0 }),
1123 FeeRate::ZERO,
1124 Version::Two,
1125 );
1126 assert_eq!(
1127 url,
1128 Url::parse(
1129 "http://localhost?v=2&additionalfeeoutputindex=0&maxadditionalfeecontribution=1000"
1130 )?
1131 );
1132 Ok(())
1133 }
1134
1135 mod bip78_checklist {
1139 use super::*;
1140
1141 #[test]
1142 fn test_transaction_versions_dont_match() -> Result<(), BoxError> {
1143 let ctx = create_psbt_context()?;
1144 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1145
1146 let original_version = ctx.original_psbt.unsigned_tx.version;
1147 let proposed_version = bitcoin::transaction::Version::non_standard(88);
1148 proposal.unsigned_tx.version = proposed_version;
1149
1150 assert!(matches!(
1151 ctx.process_proposal(proposal),
1152 Err(InternalProposalError::VersionsDontMatch {
1153 proposed,
1154 original
1155 }) if proposed == proposed_version && original == original_version
1156 ));
1157 Ok(())
1158 }
1159
1160 #[test]
1161 fn test_transaction_locktimes_dont_match() -> Result<(), BoxError> {
1162 let ctx = create_psbt_context()?;
1163 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1164
1165 let original_locktime = ctx.original_psbt.unsigned_tx.lock_time;
1166 let proposed_locktime = LockTime::from_consensus(
1167 ctx.original_psbt.unsigned_tx.lock_time.to_consensus_u32() - 1,
1168 );
1169 proposal.unsigned_tx.lock_time = proposed_locktime;
1170
1171 assert!(matches!(
1172 ctx.process_proposal(proposal),
1173 Err(InternalProposalError::LockTimesDontMatch {
1174 proposed,
1175 original
1176 }) if proposed == proposed_locktime && original == original_locktime
1177 ));
1178 Ok(())
1179 }
1180
1181 #[test]
1182 fn test_key_path_found_in_proposal() -> Result<(), BoxError> {
1183 let ctx = create_psbt_context()?;
1184 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1185
1186 let context = Secp256k1::new();
1188 let secret_key =
1189 SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order");
1190 proposal.inputs[0].bip32_derivation.insert(
1191 PublicKey::from_secret_key(&context, &secret_key),
1192 bitcoin::bip32::KeySource::default(),
1193 );
1194
1195 assert_eq!(
1196 ctx.process_proposal(proposal).unwrap_err().to_string(),
1197 InternalProposalError::TxInContainsKeyPaths.to_string()
1198 );
1199 Ok(())
1200 }
1201
1202 #[test]
1203 fn test_partial_sig_found_in_proposal() -> Result<(), BoxError> {
1204 let ctx = create_psbt_context()?;
1205 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1206
1207 let context = Secp256k1::new();
1209 let secret_key =
1210 SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order");
1211 proposal.inputs[0].partial_sigs.insert(
1212 PublicKey::from_secret_key(&context, &secret_key).into(),
1213 Signature::sighash_all(secret_key.sign_ecdsa(Message::from_digest([0; 32]))),
1214 );
1215
1216 assert_eq!(
1217 ctx.process_proposal(proposal).unwrap_err().to_string(),
1218 InternalProposalError::ContainsPartialSigs.to_string()
1219 );
1220 Ok(())
1221 }
1222
1223 #[test]
1224 fn test_sender_input_sequence_number_changed() -> Result<(), BoxError> {
1225 let ctx = create_psbt_context()?;
1226 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1227
1228 let original_sequence = proposal.unsigned_tx.input.first().unwrap().sequence;
1230 let proposed_sequence =
1231 Sequence::from_consensus(original_sequence.to_consensus_u32() - 1);
1232 proposal.unsigned_tx.input.get_mut(0).unwrap().sequence = proposed_sequence;
1233
1234 assert!(matches!(
1235 ctx.process_proposal(proposal),
1236 Err(InternalProposalError::SenderTxinSequenceChanged {
1237 proposed,
1238 original
1239 }) if proposed == proposed_sequence && original == original_sequence
1240 ));
1241 Ok(())
1242 }
1243
1244 #[test]
1245 fn test_sender_input_final_script_sig_is_present() -> Result<(), BoxError> {
1246 let ctx = create_psbt_context()?;
1247 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1248 proposal.inputs.get_mut(0).unwrap().final_script_sig = Some(ScriptBuf::new());
1249
1250 assert_eq!(
1251 ctx.process_proposal(proposal).unwrap_err().to_string(),
1252 InternalProposalError::SenderTxinContainsFinalScriptSig.to_string()
1253 );
1254
1255 Ok(())
1256 }
1257
1258 #[test]
1259 fn test_sender_input_final_script_witness_is_present() -> Result<(), BoxError> {
1260 let ctx = create_psbt_context()?;
1261 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1262 proposal.inputs.get_mut(0).unwrap().final_script_witness = Some(Witness::new());
1263
1264 assert_eq!(
1265 ctx.process_proposal(proposal).unwrap_err().to_string(),
1266 InternalProposalError::SenderTxinContainsFinalScriptWitness.to_string()
1267 );
1268
1269 Ok(())
1270 }
1271
1272 #[test]
1273 fn test_sender_input_non_all_sighash_type_is_rejected() -> Result<(), BoxError> {
1274 use bitcoin::psbt::PsbtSighashType;
1275 use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
1276
1277 let invalid_sighash_types = [
1281 PsbtSighashType::from(EcdsaSighashType::None),
1282 PsbtSighashType::from(EcdsaSighashType::Single),
1283 PsbtSighashType::from(EcdsaSighashType::AllPlusAnyoneCanPay),
1284 PsbtSighashType::from(EcdsaSighashType::NonePlusAnyoneCanPay),
1285 PsbtSighashType::from(EcdsaSighashType::SinglePlusAnyoneCanPay),
1286 PsbtSighashType::from(TapSighashType::None),
1287 PsbtSighashType::from(TapSighashType::Single),
1288 PsbtSighashType::from(TapSighashType::AllPlusAnyoneCanPay),
1289 PsbtSighashType::from(TapSighashType::NonePlusAnyoneCanPay),
1290 PsbtSighashType::from(TapSighashType::SinglePlusAnyoneCanPay),
1291 ];
1292
1293 for sighash_type in invalid_sighash_types {
1294 let mut original_psbt = PARSED_ORIGINAL_PSBT.clone();
1295 let payee = original_psbt.unsigned_tx.output[1].script_pubkey.clone();
1296 original_psbt.inputs[0].sighash_type = Some(sighash_type);
1297
1298 assert_eq!(
1299 PsbtContextBuilder::new(original_psbt, payee, None)
1300 .build(OutputSubstitution::Disabled)
1301 .unwrap_err()
1302 .to_string(),
1303 BuildSenderError::from(InternalBuildSenderError::OriginalTxinNonAllSighashType)
1304 .to_string(),
1305 "sighash type {sighash_type:?} should be rejected in the Original PSBT",
1306 );
1307
1308 let ctx = create_psbt_context()?;
1309 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1310 proposal.inputs.get_mut(0).unwrap().sighash_type = Some(sighash_type);
1311
1312 assert_eq!(
1313 ctx.process_proposal(proposal).unwrap_err().to_string(),
1314 InternalProposalError::SenderTxinNonAllSighashType.to_string(),
1315 "sighash type {sighash_type:?} should be rejected in the proposal",
1316 );
1317 }
1318 Ok(())
1319 }
1320
1321 #[test]
1322 fn test_sender_input_nonstandard_sighash_type_is_rejected() -> Result<(), BoxError> {
1323 use bitcoin::psbt::PsbtSighashType;
1324
1325 let nonstandard = [0x04, 0x80, 0xff, 0x100, u32::MAX];
1330
1331 for raw in nonstandard {
1332 let mut original_psbt = PARSED_ORIGINAL_PSBT.clone();
1333 let payee = original_psbt.unsigned_tx.output[1].script_pubkey.clone();
1334 original_psbt.inputs[0].sighash_type = Some(PsbtSighashType::from_u32(raw));
1335
1336 assert_eq!(
1337 PsbtContextBuilder::new(original_psbt, payee, None)
1338 .build(OutputSubstitution::Disabled)
1339 .unwrap_err()
1340 .to_string(),
1341 BuildSenderError::from(InternalBuildSenderError::OriginalTxinNonAllSighashType)
1342 .to_string(),
1343 "sighash type {raw:#x} should be rejected in the Original PSBT",
1344 );
1345
1346 let ctx = create_psbt_context()?;
1347 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1348 proposal.inputs.get_mut(0).unwrap().sighash_type =
1349 Some(PsbtSighashType::from_u32(raw));
1350
1351 assert_eq!(
1352 ctx.process_proposal(proposal).unwrap_err().to_string(),
1353 InternalProposalError::SenderTxinNonAllSighashType.to_string(),
1354 "sighash type {raw:#x} should be rejected in the proposal",
1355 );
1356 }
1357 Ok(())
1358 }
1359
1360 #[test]
1361 fn test_sender_input_all_sighash_type_is_accepted() -> Result<(), BoxError> {
1362 use bitcoin::psbt::PsbtSighashType;
1363 use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
1364
1365 let acceptable = [
1369 None,
1370 Some(PsbtSighashType::from(EcdsaSighashType::All)),
1371 Some(PsbtSighashType::from(TapSighashType::Default)),
1372 Some(PsbtSighashType::from(TapSighashType::All)),
1373 ];
1374
1375 for sighash_type in acceptable {
1376 let mut original_psbt = PARSED_ORIGINAL_PSBT.clone();
1377 let payee = original_psbt.unsigned_tx.output[1].script_pubkey.clone();
1378 original_psbt.inputs[0].sighash_type = sighash_type;
1379
1380 PsbtContextBuilder::new(original_psbt, payee, None)
1381 .build(OutputSubstitution::Disabled)
1382 .unwrap_or_else(|e| panic!("sighash type {sighash_type:?} should build: {e}"));
1383
1384 let ctx = create_psbt_context()?;
1385 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1386 proposal.inputs.get_mut(0).unwrap().sighash_type = sighash_type;
1387
1388 ctx.process_proposal(proposal).unwrap_or_else(|e| {
1389 panic!("sighash type {sighash_type:?} should be accepted: {e}")
1390 });
1391 }
1392 Ok(())
1393 }
1394
1395 #[test]
1396 fn test_receiver_input_is_not_finalized() -> Result<(), BoxError> {
1397 let ctx = create_psbt_context()?;
1398 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1399
1400 let proposed_outpoint = proposal.unsigned_tx.input.first().unwrap().previous_output;
1402 proposal.unsigned_tx.input.get_mut(0).unwrap().previous_output =
1403 OutPoint::new(proposed_outpoint.txid, proposed_outpoint.vout + 1);
1404
1405 proposal.inputs.get_mut(0).unwrap().final_script_sig = None;
1407 proposal.inputs.get_mut(0).unwrap().final_script_witness = None;
1408
1409 assert_eq!(
1410 ctx.process_proposal(proposal).unwrap_err().to_string(),
1411 InternalProposalError::ReceiverTxinNotFinalized.to_string()
1412 );
1413
1414 Ok(())
1415 }
1416
1417 #[test]
1418 fn test_receiver_input_missing_witness_info() -> Result<(), BoxError> {
1419 let ctx = create_psbt_context()?;
1420 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1421
1422 let proposed_outpoint = proposal.unsigned_tx.input.first().unwrap().previous_output;
1424 proposal.unsigned_tx.input.get_mut(0).unwrap().previous_output =
1425 OutPoint::new(proposed_outpoint.txid, proposed_outpoint.vout + 1);
1426 proposal.inputs.get_mut(0).unwrap().final_script_sig = Some(ScriptBuf::new());
1427 proposal.inputs.get_mut(0).unwrap().final_script_witness = Some(Witness::new());
1428
1429 proposal.inputs.get_mut(0).unwrap().witness_utxo = None;
1431 proposal.inputs.get_mut(0).unwrap().non_witness_utxo = None;
1432
1433 assert_eq!(
1434 ctx.process_proposal(proposal).unwrap_err().to_string(),
1435 InternalProposalError::ReceiverTxinMissingUtxoInfo.to_string()
1436 );
1437
1438 Ok(())
1439 }
1440
1441 #[test]
1442 fn test_receiver_input_has_mixed_sequence_() -> Result<(), BoxError> {
1443 let mut ctx = create_psbt_context()?;
1444 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1445
1446 let proposed_outpoint = proposal.unsigned_tx.input.first().unwrap().previous_output;
1448 proposal.unsigned_tx.input.get_mut(0).unwrap().previous_output =
1449 OutPoint::new(proposed_outpoint.txid, proposed_outpoint.vout + 1);
1450 proposal.inputs.get_mut(0).unwrap().final_script_sig = Some(ScriptBuf::new());
1451 proposal.inputs.get_mut(0).unwrap().final_script_witness = Some(Witness::new());
1452
1453 let sequence = ctx.original_psbt.unsigned_tx.input.get_mut(0).unwrap().sequence;
1455 proposal.unsigned_tx.input.get_mut(0).unwrap().sequence =
1456 Sequence::from_consensus(sequence.to_consensus_u32() + 1);
1457
1458 assert_eq!(
1459 ctx.process_proposal(proposal).unwrap_err().to_string(),
1460 InternalProposalError::MixedSequence.to_string()
1461 );
1462
1463 Ok(())
1464 }
1465
1466 #[test]
1467 fn test_process_proposal_when_missing_original_inputs() -> Result<(), BoxError> {
1468 let ctx = create_psbt_context()?;
1469 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1470
1471 proposal.unsigned_tx.input.clear();
1472 proposal.inputs.clear();
1473
1474 assert_eq!(
1475 ctx.process_proposal(proposal).unwrap_err().to_string(),
1476 InternalProposalError::MissingOrShuffledInputs.to_string()
1477 );
1478
1479 Ok(())
1480 }
1481
1482 #[test]
1483 fn test_process_proposal_when_output_contains_key_path() -> Result<(), BoxError> {
1484 let ctx = create_psbt_context()?;
1485 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1486
1487 let context = Secp256k1::new();
1488 let secret_key =
1489 SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order");
1490 proposal.outputs.get_mut(0).unwrap().bip32_derivation.insert(
1491 PublicKey::from_secret_key(&context, &secret_key),
1492 bitcoin::bip32::KeySource::default(),
1493 );
1494
1495 assert_eq!(
1496 ctx.process_proposal(proposal).unwrap_err().to_string(),
1497 InternalProposalError::TxOutContainsKeyPaths.to_string()
1498 );
1499
1500 Ok(())
1501 }
1502
1503 #[test]
1504 fn test_receiver_steals_sender_change() -> Result<(), BoxError> {
1505 let ctx = create_psbt_context()?;
1506 let mut proposal = PARSED_PAYJOIN_PROPOSAL.clone();
1507 proposal.unsigned_tx.output[0].value -= Amount::from_btc(0.5)?;
1509 proposal.unsigned_tx.output[1].value += Amount::from_btc(0.5)?;
1510
1511 assert_eq!(
1512 ctx.process_proposal(proposal).unwrap_err().to_string(),
1513 InternalProposalError::FeeContributionExceedsMaximum.to_string()
1514 );
1515
1516 Ok(())
1517 }
1518
1519 #[test]
1520 fn test_process_proposal_when_payee_output_has_disallowed_output_substitution(
1521 ) -> Result<(), BoxError> {
1522 let mut ctx = create_psbt_context()?;
1523 let mut proposal = PARSED_PAYJOIN_PROPOSAL.clone();
1524 ctx.output_substitution = OutputSubstitution::Disabled;
1525
1526 assert!(ctx.clone().process_proposal(proposal.clone()).is_ok());
1528
1529 proposal.unsigned_tx.output[0].value += MAX_ADDITIONAL_FEE_CONTRIBUTION;
1531 assert!(ctx.clone().process_proposal(proposal.clone()).is_ok());
1532
1533 proposal.unsigned_tx.output[0].value -= MAX_ADDITIONAL_FEE_CONTRIBUTION;
1534 ctx.original_psbt.unsigned_tx.output.get_mut(0).unwrap().script_pubkey =
1535 ctx.payee.clone();
1536 std::mem::swap(
1537 &mut ctx.original_psbt.unsigned_tx.output[0].value,
1538 &mut proposal.unsigned_tx.output[0].value,
1539 );
1540 assert_eq!(
1541 ctx.process_proposal(proposal).unwrap_err().to_string(),
1542 InternalProposalError::DisallowedOutputSubstitution.to_string()
1543 );
1544
1545 Ok(())
1546 }
1547
1548 #[test]
1549 fn test_process_proposal_when_payee_output_has_allowed_output_substitution(
1550 ) -> Result<(), BoxError> {
1551 let mut ctx = create_psbt_context()?;
1552 let mut proposal = PARSED_PAYJOIN_PROPOSAL.clone();
1553
1554 ctx.output_substitution = OutputSubstitution::Enabled;
1556 ctx.original_psbt.unsigned_tx.output.get_mut(0).unwrap().script_pubkey =
1557 ctx.payee.clone();
1558 assert!(ctx.clone().process_proposal(proposal.clone()).is_ok());
1559
1560 proposal.unsigned_tx.output[0].value += MAX_ADDITIONAL_FEE_CONTRIBUTION;
1561 assert!(ctx.clone().process_proposal(proposal.clone()).is_ok());
1562
1563 proposal.unsigned_tx.output[0].value -= Amount::from_sat(364);
1564 assert!(ctx.process_proposal(proposal).is_ok());
1565
1566 Ok(())
1567 }
1568
1569 #[test]
1570 fn test_process_proposal_when_output_value_decreased() -> Result<(), BoxError> {
1571 let mut ctx = create_psbt_context()?;
1572 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1573
1574 ctx.fee_contribution = None;
1575 proposal.unsigned_tx.output.get_mut(0).unwrap().value =
1576 ctx.original_psbt.unsigned_tx.output.get_mut(0).unwrap().value
1577 - Amount::from_sat(1);
1578
1579 assert_eq!(
1580 ctx.process_proposal(proposal).unwrap_err().to_string(),
1581 InternalProposalError::OutputValueDecreased.to_string()
1582 );
1583
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn test_process_proposal_rejects_different_script_in_our_output() -> Result<(), BoxError> {
1589 let mut ctx = create_psbt_context()?;
1590 ctx.fee_contribution = None;
1591 ctx.output_substitution = OutputSubstitution::Enabled;
1592
1593 let mut proposal = PARSED_PAYJOIN_PROPOSAL.clone();
1594 let original_change =
1595 &ctx.original_psbt.unsigned_tx.output[ADDITIONAL_FEE_OUTPUT_INDEX];
1596 assert_ne!(original_change.script_pubkey, ctx.payee);
1597
1598 let change_pos = proposal
1599 .unsigned_tx
1600 .output
1601 .iter()
1602 .position(|o| o.script_pubkey == original_change.script_pubkey)
1603 .expect("proposal should contain the change output");
1604
1605 let dummy_script =
1609 ScriptBuf::from_hex("0014ffffffffffffffffffffffffffffffffffffffffff")?;
1610 proposal.unsigned_tx.output[change_pos] =
1611 TxOut { value: original_change.value, script_pubkey: dummy_script };
1612 assert!(
1613 ctx.process_proposal(proposal).is_err(),
1614 "arm 3 must reject script mismatch in our output"
1615 );
1616
1617 Ok(())
1618 }
1619
1620 #[test]
1621 fn test_process_proposal_when_output_missing() -> Result<(), BoxError> {
1622 let ctx = create_psbt_context()?;
1623 let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();
1624
1625 proposal.unsigned_tx.output.clear();
1626 proposal.outputs.clear();
1627
1628 assert_eq!(
1629 ctx.process_proposal(proposal).unwrap_err().to_string(),
1630 InternalProposalError::MissingOrShuffledOutputs.to_string()
1631 );
1632
1633 Ok(())
1634 }
1635
1636 #[test]
1637 fn test_absolute_fee_less_than_original_psbt() -> Result<(), BoxError> {
1638 let ctx = create_psbt_context()?;
1639 let mut proposal = PARSED_PAYJOIN_PROPOSAL.clone();
1640 for output in proposal.outputs_mut() {
1641 output.bip32_derivation.clear();
1642 }
1643 for input in proposal.inputs_mut() {
1644 input.bip32_derivation.clear();
1645 }
1646
1647 proposal.unsigned_tx.output[0].value += bitcoin::Amount::from_sat(183);
1649
1650 assert_eq!(
1651 ctx.process_proposal(proposal).unwrap_err().to_string(),
1652 InternalProposalError::AbsoluteFeeDecreased.to_string()
1653 );
1654
1655 Ok(())
1656 }
1657
1658 #[test]
1659 fn test_payee_took_contributed_fee() -> Result<(), BoxError> {
1660 let ctx = create_psbt_context()?;
1661 let mut proposal = ctx.original_psbt.clone();
1662
1663 for input in proposal.inputs_mut() {
1664 input.bip32_derivation.clear();
1665 input.partial_sigs.clear();
1666 input.final_script_sig = None;
1667 input.final_script_witness = None;
1668 }
1669
1670 let redistributed_amount = Amount::from_sat(1);
1671
1672 let output_0 = proposal.unsigned_tx.output[0].value;
1674 proposal.unsigned_tx.output[0].value = output_0 - redistributed_amount;
1675 let output_1 = proposal.unsigned_tx.output[1].value;
1676 proposal.unsigned_tx.output[1].value = output_1 + redistributed_amount;
1677
1678 assert_eq!(
1679 ctx.process_proposal(proposal).unwrap_err().to_string(),
1680 InternalProposalError::PayeeTookContributedFee.to_string()
1681 );
1682
1683 Ok(())
1684 }
1685
1686 #[test]
1687 fn test_fee_contribution_pays_output_size_increase() -> Result<(), BoxError> {
1688 let ctx = create_psbt_context()?;
1689 let mut proposal = ctx.original_psbt.clone();
1690
1691 for input in proposal.inputs_mut() {
1692 input.bip32_derivation.clear();
1693 input.partial_sigs.clear();
1694 input.final_script_sig = None;
1695 input.final_script_witness = None;
1696 }
1697
1698 let contributed_fee = Amount::from_sat(10);
1699 let original_output = proposal.unsigned_tx.output[0].value;
1700 proposal.unsigned_tx.output[0].value = original_output - contributed_fee;
1701
1702 assert_eq!(
1703 ctx.process_proposal(proposal).unwrap_err().to_string(),
1704 InternalProposalError::FeeContributionPaysOutputSizeIncrease.to_string()
1705 );
1706
1707 Ok(())
1708 }
1709
1710 #[test]
1711 fn test_fee_rate_below_minimum() -> Result<(), BoxError> {
1712 let mut ctx = create_psbt_context()?;
1713 let mut proposal = ctx.original_psbt.clone();
1714
1715 for input in proposal.inputs_mut() {
1716 input.bip32_derivation.clear();
1717 input.partial_sigs.clear();
1718 input.final_script_sig = None;
1719 input.final_script_witness = None;
1720 }
1721
1722 ctx.min_fee_rate = FeeRate::MAX;
1724
1725 assert_eq!(
1726 ctx.process_proposal(proposal).unwrap_err().to_string(),
1727 InternalProposalError::FeeRateBelowMinimum.to_string()
1728 );
1729
1730 Ok(())
1731 }
1732 }
1733}