Skip to content

Commit dca7f73

Browse files
committed
Update v2::SenderBuilder to ignore pjos
V2 receivers should always signal `pjos=0` to preserve backwards compatibility with V1 senders, since V1 communication is unauthenticated and goes through an untrusted payjoin directory. V2 senders may safely ignore this flag because all communications are end-to-end authenticated and allowing output substitution enables more interesting use cases for receivers. A V2 sender may still signal their own output substitution preference via the `disableoutputsubstitution` request param.
1 parent b9dfd24 commit dca7f73

File tree

1 file changed

+67
-21
lines changed
  • payjoin/src/core/send/v2

1 file changed

+67
-21
lines changed

payjoin/src/core/send/v2/mod.rs

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,7 @@ impl<'a> SenderBuilder<'a> {
7575
self,
7676
min_fee_rate: FeeRate,
7777
) -> MaybeBadInitInputsTransition<SessionEvent, Sender<WithReplyKey>, BuildSenderError> {
78-
let v1 = match self.0.build_recommended(min_fee_rate) {
79-
Ok(inner) => inner,
80-
Err(e) => return MaybeBadInitInputsTransition::bad_init_inputs(e),
81-
};
82-
let with_reply_key = WithReplyKey { v1, reply_key: HpkeKeyPair::gen_keypair().0 };
83-
MaybeBadInitInputsTransition::success(
84-
SessionEvent::CreatedReplyKey(with_reply_key.clone()),
85-
Sender { state: with_reply_key },
86-
)
78+
self.v2_sender_from_v1(self.0.clone().build_recommended(min_fee_rate))
8779
}
8880

8981
/// Offer the receiver contribution to pay for his input.
@@ -106,21 +98,12 @@ impl<'a> SenderBuilder<'a> {
10698
min_fee_rate: FeeRate,
10799
clamp_fee_contribution: bool,
108100
) -> MaybeBadInitInputsTransition<SessionEvent, Sender<WithReplyKey>, BuildSenderError> {
109-
let v1 = match self.0.build_with_additional_fee(
101+
self.v2_sender_from_v1(self.0.clone().build_with_additional_fee(
110102
max_fee_contribution,
111103
change_index,
112104
min_fee_rate,
113105
clamp_fee_contribution,
114-
) {
115-
Ok(inner) => inner,
116-
Err(e) => return MaybeBadInitInputsTransition::bad_init_inputs(e),
117-
};
118-
119-
let with_reply_key = WithReplyKey { v1, reply_key: HpkeKeyPair::gen_keypair().0 };
120-
MaybeBadInitInputsTransition::success(
121-
SessionEvent::CreatedReplyKey(with_reply_key.clone()),
122-
Sender { state: with_reply_key },
123-
)
106+
))
124107
}
125108

126109
/// Perform Payjoin without incentivizing the payee to cooperate.
@@ -131,11 +114,26 @@ impl<'a> SenderBuilder<'a> {
131114
self,
132115
min_fee_rate: FeeRate,
133116
) -> MaybeBadInitInputsTransition<SessionEvent, Sender<WithReplyKey>, BuildSenderError> {
134-
let v1 = match self.0.build_non_incentivizing(min_fee_rate) {
117+
self.v2_sender_from_v1(self.0.clone().build_non_incentivizing(min_fee_rate))
118+
}
119+
120+
/// Helper function that takes a V1 sender build result and wraps it in a V2 Sender,
121+
/// returning the appropriate state transition.
122+
fn v2_sender_from_v1(
123+
&self,
124+
v1_result: Result<v1::Sender, BuildSenderError>,
125+
) -> MaybeBadInitInputsTransition<SessionEvent, Sender<WithReplyKey>, BuildSenderError> {
126+
let mut v1 = match v1_result {
135127
Ok(inner) => inner,
136128
Err(e) => return MaybeBadInitInputsTransition::bad_init_inputs(e),
137129
};
138130

131+
// V2 senders may always ignore the receiver's `pjos` output substitution preference,
132+
// because all communications with the receiver are end-to-end authenticated.
133+
if self.0.output_substitution == OutputSubstitution::Enabled {
134+
v1.output_substitution = OutputSubstitution::Enabled;
135+
}
136+
139137
let with_reply_key = WithReplyKey { v1, reply_key: HpkeKeyPair::gen_keypair().0 };
140138
MaybeBadInitInputsTransition::success(
141139
SessionEvent::CreatedReplyKey(with_reply_key.clone()),
@@ -495,9 +493,12 @@ mod test {
495493
use std::time::{Duration, SystemTime};
496494

497495
use bitcoin::hex::FromHex;
496+
use bitcoin::Address;
498497
use payjoin_test_utils::{BoxError, EXAMPLE_URL, KEM, KEY_ID, PARSED_ORIGINAL_PSBT, SYMMETRIC};
499498

500499
use super::*;
500+
use crate::persist::NoopSessionPersister;
501+
use crate::receive::v2::Receiver;
501502
use crate::OhttpKeys;
502503

503504
const SERIALIZED_BODY_V2: &str = "63484e696450384241484d43414141414159386e757447674a647959475857694245623435486f65396c5747626b78682f36624e694f4a6443447544414141414141442b2f2f2f2f41747956754155414141414146366b554865684a38476e536442554f4f7636756a584c72576d734a5244434867495165414141414141415871525233514a62627a30686e513849765130667074476e2b766f746e656f66544141414141414542494b6762317755414141414146366b55336b34656b47484b57524e6241317256357452356b455644564e4348415163584667415578347046636c4e56676f31575741644e3153594e583874706854414243477343527a424541694238512b41366465702b527a393276687932366c5430416a5a6e3450524c6938426639716f422f434d6b30774967502f526a3250575a3367456a556b546c6844524e415130675877544f3774396e2b563134705a366f6c6a554249514d566d7341616f4e5748564d5330324c6654536530653338384c4e697450613155515a794f6968592b464667414241425941464562324769753663344b4f35595730706677336c4770396a4d55554141413d0a763d32";
@@ -614,4 +615,49 @@ mod test {
614615
}
615616
Ok(())
616617
}
618+
619+
#[test]
620+
fn test_v2_sender_builder() {
621+
let address = Address::from_str("2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7")
622+
.expect("valid address")
623+
.assume_checked();
624+
let directory = EXAMPLE_URL.clone();
625+
let ohttp_keys = OhttpKeys(
626+
ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
627+
);
628+
let pj_uri = Receiver::create_session(address.clone(), directory, ohttp_keys, None)
629+
.save(&NoopSessionPersister::default())
630+
.expect("receiver should succeed")
631+
.pj_uri();
632+
let req_ctx = SenderBuilder::new(PARSED_ORIGINAL_PSBT.clone(), pj_uri.clone())
633+
.build_recommended(FeeRate::BROADCAST_MIN)
634+
.save(&NoopSessionPersister::default())
635+
.expect("sender should succeed");
636+
// v2 senders may always override the receiver's `pjos` parameter to enable output
637+
// substitution
638+
assert_eq!(req_ctx.v1.output_substitution, OutputSubstitution::Enabled);
639+
assert_eq!(&req_ctx.v1.payee, &address.script_pubkey());
640+
let fee_contribution = req_ctx.v1.fee_contribution.expect("sender should contribute fees");
641+
assert_eq!(fee_contribution.max_amount, Amount::from_sat(91));
642+
assert_eq!(fee_contribution.vout, 0);
643+
assert_eq!(req_ctx.v1.min_fee_rate, FeeRate::from_sat_per_kwu(250));
644+
// ensure that the other builder methods also enable output substitution
645+
let req_ctx = SenderBuilder::new(PARSED_ORIGINAL_PSBT.clone(), pj_uri.clone())
646+
.build_non_incentivizing(FeeRate::BROADCAST_MIN)
647+
.save(&NoopSessionPersister::default())
648+
.expect("sender should succeed");
649+
assert_eq!(req_ctx.v1.output_substitution, OutputSubstitution::Enabled);
650+
let req_ctx = SenderBuilder::new(PARSED_ORIGINAL_PSBT.clone(), pj_uri.clone())
651+
.build_with_additional_fee(Amount::ZERO, Some(0), FeeRate::BROADCAST_MIN, false)
652+
.save(&NoopSessionPersister::default())
653+
.expect("sender should succeed");
654+
assert_eq!(req_ctx.v1.output_substitution, OutputSubstitution::Enabled);
655+
// ensure that a v2 sender may still disable output substitution if they prefer.
656+
let req_ctx = SenderBuilder::new(PARSED_ORIGINAL_PSBT.clone(), pj_uri)
657+
.always_disable_output_substitution()
658+
.build_recommended(FeeRate::BROADCAST_MIN)
659+
.save(&NoopSessionPersister::default())
660+
.expect("sender should succeed");
661+
assert_eq!(req_ctx.v1.output_substitution, OutputSubstitution::Disabled);
662+
}
617663
}

0 commit comments

Comments
 (0)