Skip to content

Commit f5982cf

Browse files
authored
Correct the pjos parameter parity (payjoin#546)
alter and corrected the logic for `pjos` parameter fixes payjoin#545
2 parents 8c2a6b5 + 4889b1e commit f5982cf

File tree

13 files changed

+205
-90
lines changed

13 files changed

+205
-90
lines changed

payjoin-cli/src/app/v1.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,11 @@ impl App {
139139
let pj_part = payjoin::Url::parse(pj_part)
140140
.map_err(|e| anyhow!("Failed to parse pj_endpoint: {}", e))?;
141141

142-
let mut pj_uri =
143-
payjoin::receive::v1::build_v1_pj_uri(&pj_receiver_address, &pj_part, false)?;
142+
let mut pj_uri = payjoin::receive::v1::build_v1_pj_uri(
143+
&pj_receiver_address,
144+
&pj_part,
145+
payjoin::OutputSubstitution::Enabled,
146+
)?;
144147
pj_uri.amount = Some(amount);
145148

146149
Ok(pj_uri.to_string())

payjoin/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ mod request;
5454
#[cfg(feature = "_core")]
5555
pub use request::*;
5656
#[cfg(feature = "_core")]
57+
pub(crate) mod output_substitution;
58+
#[cfg(feature = "v1")]
59+
pub use output_substitution::OutputSubstitution;
60+
#[cfg(feature = "_core")]
5761
mod uri;
58-
5962
#[cfg(feature = "_core")]
6063
pub use into_url::{Error as IntoUrlError, IntoUrl};
6164
#[cfg(feature = "_core")]

payjoin/src/output_substitution.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// Whether the receiver is allowed to substitute original outputs or not.
2+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3+
#[cfg_attr(feature = "v2", derive(serde::Serialize, serde::Deserialize))]
4+
pub enum OutputSubstitution {
5+
Enabled,
6+
Disabled,
7+
}
8+
9+
impl OutputSubstitution {
10+
/// Combine two output substitution flags.
11+
///
12+
/// If both are enabled, the result is enabled.
13+
/// If one is disabled, the result is disabled.
14+
pub(crate) fn combine(self, other: Self) -> Self {
15+
match (self, other) {
16+
(Self::Enabled, Self::Enabled) => Self::Enabled,
17+
_ => Self::Disabled,
18+
}
19+
}
20+
}

payjoin/src/receive/optional_parameters.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use std::fmt;
44
use bitcoin::FeeRate;
55
use log::warn;
66

7+
use crate::output_substitution::OutputSubstitution;
8+
79
#[derive(Debug, Clone)]
810
pub(crate) struct Params {
911
// version
1012
pub v: usize,
1113
// disableoutputsubstitution
12-
pub disable_output_substitution: bool,
14+
pub output_substitution: OutputSubstitution,
1315
// maxadditionalfeecontribution, additionalfeeoutputindex
1416
pub additional_fee_contribution: Option<(bitcoin::Amount, usize)>,
1517
// minfeerate
@@ -23,7 +25,7 @@ impl Default for Params {
2325
fn default() -> Self {
2426
Params {
2527
v: 1,
26-
disable_output_substitution: false,
28+
output_substitution: OutputSubstitution::Enabled,
2729
additional_fee_contribution: None,
2830
min_fee_rate: FeeRate::BROADCAST_MIN,
2931
#[cfg(feature = "_multiparty")]
@@ -88,7 +90,11 @@ impl Params {
8890
Err(_) => return Err(Error::FeeRate),
8991
},
9092
("disableoutputsubstitution", v) =>
91-
params.disable_output_substitution = v == "true",
93+
params.output_substitution = if v == "true" {
94+
OutputSubstitution::Disabled
95+
} else {
96+
OutputSubstitution::Enabled
97+
},
9298
#[cfg(feature = "_multiparty")]
9399
("optimisticmerge", v) => params.optimistic_merge = v == "true",
94100
_ => (),

payjoin/src/receive/v1/exclusive/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ pub trait Headers {
1616
pub fn build_v1_pj_uri<'a>(
1717
address: &bitcoin::Address,
1818
endpoint: impl IntoUrl,
19-
disable_output_substitution: bool,
19+
output_substitution: OutputSubstitution,
2020
) -> Result<crate::uri::PjUri<'a>, crate::into_url::Error> {
21-
let extras =
22-
crate::uri::PayjoinExtras { endpoint: endpoint.into_url()?, disable_output_substitution };
21+
let extras = crate::uri::PayjoinExtras { endpoint: endpoint.into_url()?, output_substitution };
2322
Ok(bitcoin_uri::Uri::with_extras(address.clone(), extras))
2423
}
2524

payjoin/src/receive/v1/mod.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use super::optional_parameters::Params;
3939
use super::{
4040
ImplementationError, InputPair, OutputSubstitutionError, ReplyableError, SelectionError,
4141
};
42+
use crate::output_substitution::OutputSubstitution;
4243
use crate::psbt::PsbtExt;
4344
use crate::receive::InternalPayloadError;
4445

@@ -264,9 +265,8 @@ pub struct WantsOutputs {
264265
}
265266

266267
impl WantsOutputs {
267-
pub fn is_output_substitution_disabled(&self) -> bool {
268-
self.params.disable_output_substitution
269-
}
268+
/// Whether the receiver is allowed to substitute original outputs or not.
269+
pub fn output_substitution(&self) -> OutputSubstitution { self.params.output_substitution }
270270

271271
/// Substitute the receiver output script with the provided script.
272272
pub fn substitute_receiver_script(
@@ -306,7 +306,7 @@ impl WantsOutputs {
306306
// Select an output with the same address if one was provided
307307
Some(pos) => {
308308
let txo = replacement_outputs.swap_remove(pos);
309-
if self.params.disable_output_substitution
309+
if self.output_substitution() == OutputSubstitution::Disabled
310310
&& txo.value < original_output.value
311311
{
312312
return Err(
@@ -317,7 +317,7 @@ impl WantsOutputs {
317317
}
318318
// Otherwise randomly select one of the replacement outputs
319319
None => {
320-
if self.params.disable_output_substitution {
320+
if self.output_substitution() == OutputSubstitution::Disabled {
321321
return Err(
322322
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled
323323
.into(),
@@ -708,7 +708,7 @@ impl ProvisionalProposal {
708708
self.payjoin_psbt.inputs[i].tap_key_sig = None;
709709
}
710710

711-
PayjoinProposal { payjoin_psbt: self.payjoin_psbt, params: self.params }
711+
PayjoinProposal { payjoin_psbt: self.payjoin_psbt }
712712
}
713713

714714
/// Return the indexes of the sender inputs
@@ -763,18 +763,13 @@ impl ProvisionalProposal {
763763
#[derive(Debug, Clone)]
764764
pub struct PayjoinProposal {
765765
payjoin_psbt: Psbt,
766-
params: Params,
767766
}
768767

769768
impl PayjoinProposal {
770769
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
771770
self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
772771
}
773772

774-
pub fn is_output_substitution_disabled(&self) -> bool {
775-
self.params.disable_output_substitution
776-
}
777-
778773
pub fn psbt(&self) -> &Psbt { &self.payjoin_psbt }
779774
}
780775

@@ -949,8 +944,7 @@ pub(crate) mod test {
949944
#[test]
950945
fn test_pjos_disabled() {
951946
let mut proposal = proposal_from_test_vector().unwrap();
952-
// Specify outputsubstitution is disabled
953-
proposal.params.disable_output_substitution = true;
947+
proposal.params.output_substitution = OutputSubstitution::Disabled;
954948
let wants_outputs = wants_outputs_from_test_vector(proposal).unwrap();
955949

956950
let output_value =

payjoin/src/receive/v2/mod.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use super::{
1818
};
1919
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
2020
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys};
21+
use crate::output_substitution::OutputSubstitution;
2122
use crate::receive::{parse_payload, InputPair};
2223
use crate::uri::ShortId;
2324
use crate::{IntoUrl, IntoUrlError, Request};
@@ -192,7 +193,7 @@ impl Receiver {
192193
//
193194
// see: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#unsecured-payjoin-server
194195
if params.v == 1 {
195-
params.disable_output_substitution = true;
196+
params.output_substitution = OutputSubstitution::Disabled;
196197

197198
// Additionally V1 sessions never have an optimistic merge opportunity
198199
#[cfg(feature = "_multiparty")]
@@ -212,7 +213,8 @@ impl Receiver {
212213
pj.set_receiver_pubkey(self.context.s.public_key().clone());
213214
pj.set_ohttp(self.context.ohttp_keys.clone());
214215
pj.set_exp(self.context.expiry);
215-
let extras = PayjoinExtras { endpoint: pj, disable_output_substitution: false };
216+
let extras =
217+
PayjoinExtras { endpoint: pj, output_substitution: OutputSubstitution::Enabled };
216218
bitcoin_uri::Uri::with_extras(self.context.address.clone(), extras)
217219
}
218220

@@ -385,9 +387,8 @@ pub struct WantsOutputs {
385387
}
386388

387389
impl WantsOutputs {
388-
pub fn is_output_substitution_disabled(&self) -> bool {
389-
self.v1.is_output_substitution_disabled()
390-
}
390+
/// Whether the receiver is allowed to substitute original outputs or not.
391+
pub fn output_substitution(&self) -> OutputSubstitution { self.v1.output_substitution() }
391392

392393
/// Substitute the receiver output script with the provided script.
393394
pub fn substitute_receiver_script(
@@ -503,10 +504,6 @@ impl PayjoinProposal {
503504
self.v1.utxos_to_be_locked()
504505
}
505506

506-
pub fn is_output_substitution_disabled(&self) -> bool {
507-
self.v1.is_output_substitution_disabled()
508-
}
509-
510507
pub fn psbt(&self) -> &Psbt { self.v1.psbt() }
511508

512509
pub fn extract_v2_req(
@@ -652,6 +649,6 @@ mod test {
652649
fn test_v2_pj_uri() {
653650
let uri = Receiver { context: SHARED_CONTEXT.clone() }.pj_uri();
654651
assert_ne!(uri.extras.endpoint, EXAMPLE_URL.clone());
655-
assert!(!uri.extras.disable_output_substitution);
652+
assert_eq!(uri.extras.output_substitution, OutputSubstitution::Enabled);
656653
}
657654
}

payjoin/src/send/mod.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub use error::{BuildSenderError, ResponseError, ValidationError, WellKnownError
1717
pub(crate) use error::{InternalBuildSenderError, InternalProposalError, InternalValidationError};
1818
use url::Url;
1919

20+
use crate::output_substitution::OutputSubstitution;
2021
use crate::psbt::PsbtExt;
2122

2223
// See usize casts
@@ -51,7 +52,7 @@ pub(crate) struct AdditionalFeeContribution {
5152
#[derive(Debug, Clone)]
5253
pub struct PsbtContext {
5354
original_psbt: Psbt,
54-
disable_output_substitution: bool,
55+
output_substitution: OutputSubstitution,
5556
fee_contribution: Option<AdditionalFeeContribution>,
5657
min_fee_rate: FeeRate,
5758
payee: ScriptBuf,
@@ -253,7 +254,7 @@ impl PsbtContext {
253254
if original_output.script_pubkey == self.payee =>
254255
{
255256
ensure!(
256-
!self.disable_output_substitution
257+
self.output_substitution == OutputSubstitution::Enabled
257258
|| (proposed_txout.script_pubkey == original_output.script_pubkey
258259
&& proposed_txout.value >= original_output.value),
259260
DisallowedOutputSubstitution
@@ -414,14 +415,14 @@ fn determine_fee_contribution(
414415

415416
fn serialize_url(
416417
endpoint: Url,
417-
disable_output_substitution: bool,
418+
output_substitution: OutputSubstitution,
418419
fee_contribution: Option<AdditionalFeeContribution>,
419420
min_fee_rate: FeeRate,
420421
version: &str,
421422
) -> Url {
422423
let mut url = endpoint;
423424
url.query_pairs_mut().append_pair("v", version);
424-
if disable_output_substitution {
425+
if output_substitution == OutputSubstitution::Disabled {
425426
url.query_pairs_mut().append_pair("disableoutputsubstitution", "true");
426427
}
427428
if let Some(AdditionalFeeContribution { max_amount, vout }) = fee_contribution {
@@ -449,14 +450,15 @@ mod test {
449450
use super::{
450451
check_single_payee, clear_unneeded_fields, determine_fee_contribution, serialize_url,
451452
};
453+
use crate::output_substitution::OutputSubstitution;
452454
use crate::psbt::PsbtExt;
453455
use crate::send::{AdditionalFeeContribution, InternalBuildSenderError, InternalProposalError};
454456

455457
pub(crate) fn create_psbt_context() -> Result<super::PsbtContext, BoxError> {
456458
let payee = PARSED_ORIGINAL_PSBT.unsigned_tx.output[1].script_pubkey.clone();
457459
Ok(super::PsbtContext {
458460
original_psbt: PARSED_ORIGINAL_PSBT.clone(),
459-
disable_output_substitution: false,
461+
output_substitution: OutputSubstitution::Enabled,
460462
fee_contribution: Some(AdditionalFeeContribution {
461463
max_amount: bitcoin::Amount::from_sat(182),
462464
vout: 0,
@@ -641,10 +643,22 @@ mod test {
641643

642644
#[test]
643645
fn test_disable_output_substitution_query_param() -> Result<(), BoxError> {
644-
let url = serialize_url(Url::parse("http://localhost")?, true, None, FeeRate::ZERO, "2");
646+
let url = serialize_url(
647+
Url::parse("http://localhost")?,
648+
OutputSubstitution::Disabled,
649+
None,
650+
FeeRate::ZERO,
651+
"2",
652+
);
645653
assert_eq!(url, Url::parse("http://localhost?v=2&disableoutputsubstitution=true")?);
646654

647-
let url = serialize_url(Url::parse("http://localhost")?, false, None, FeeRate::ZERO, "2");
655+
let url = serialize_url(
656+
Url::parse("http://localhost")?,
657+
OutputSubstitution::Enabled,
658+
None,
659+
FeeRate::ZERO,
660+
"2",
661+
);
648662
assert_eq!(url, Url::parse("http://localhost?v=2")?);
649663
Ok(())
650664
}

0 commit comments

Comments
 (0)