Skip to content

Commit 8c2a6b5

Browse files
authored
Replace OutputSubstitutionDisabled string context with sub variants (payjoin#590)
closes payjoin#588
2 parents 68bdf3e + 6bd53d8 commit 8c2a6b5

File tree

4 files changed

+107
-21
lines changed

4 files changed

+107
-21
lines changed

payjoin-test-utils/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ pub const SYMMETRIC: &[SymmetricSuite] =
266266
// |-----------------|-----------------------|------------------------------|-------------------------|
267267
// | P2SH-P2WPKH | 2 sat/vbyte | 0.00000182 | 0 |
268268

269+
pub const QUERY_PARAMS: &str = "maxadditionalfeecontribution=182&additionalfeeoutputindex=0";
270+
269271
pub const ORIGINAL_PSBT: &str = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
270272

271273
pub const PAYJOIN_PROPOSAL: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=";

payjoin/src/receive/error.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,15 @@ impl std::error::Error for PayloadError {
265265
///
266266
/// This is currently opaque type because we aren't sure which variants will stay.
267267
/// You can only display it.
268-
#[derive(Debug)]
268+
#[derive(Debug, PartialEq)]
269269
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
270270

271-
#[derive(Debug)]
271+
#[derive(Debug, PartialEq)]
272272
pub(crate) enum InternalOutputSubstitutionError {
273-
/// Output substitution is disabled
274-
OutputSubstitutionDisabled(&'static str),
273+
/// Output substitution is disabled and output value was decreased
274+
DecreasedValueWhenDisabled,
275+
/// Output substitution is disabled and script pubkey was changed
276+
ScriptPubKeyChangedWhenDisabled,
275277
/// Current output substitution implementation doesn't support reducing the number of outputs
276278
NotEnoughOutputs,
277279
/// The provided drain script could not be identified in the provided replacement outputs
@@ -281,7 +283,8 @@ pub(crate) enum InternalOutputSubstitutionError {
281283
impl fmt::Display for OutputSubstitutionError {
282284
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
283285
match &self.0 {
284-
InternalOutputSubstitutionError::OutputSubstitutionDisabled(reason) => write!(f, "{}", &format!("Output substitution is disabled: {}", reason)),
286+
InternalOutputSubstitutionError::DecreasedValueWhenDisabled => write!(f, "Decreasing the receiver output value is not allowed when output substitution is disabled"),
287+
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => write!(f, "Changing the receiver output script pubkey is not allowed when output substitution is disabled"),
285288
InternalOutputSubstitutionError::NotEnoughOutputs => write!(
286289
f,
287290
"Current output substitution implementation doesn't support reducing the number of outputs"
@@ -299,7 +302,8 @@ impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
299302
impl std::error::Error for OutputSubstitutionError {
300303
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
301304
match &self.0 {
302-
InternalOutputSubstitutionError::OutputSubstitutionDisabled(_) => None,
305+
InternalOutputSubstitutionError::DecreasedValueWhenDisabled => None,
306+
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled => None,
303307
InternalOutputSubstitutionError::NotEnoughOutputs => None,
304308
InternalOutputSubstitutionError::InvalidDrainScript => None,
305309
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ fn parse_body(
7171
#[cfg(test)]
7272
mod tests {
7373
use bitcoin::{Address, AddressType};
74-
use payjoin_test_utils::ORIGINAL_PSBT;
74+
use payjoin_test_utils::{ORIGINAL_PSBT, QUERY_PARAMS};
7575

7676
use super::*;
7777
struct MockHeaders {
@@ -96,7 +96,7 @@ mod tests {
9696
fn test_from_request() -> Result<(), Box<dyn std::error::Error>> {
9797
let body = ORIGINAL_PSBT.as_bytes();
9898
let headers = MockHeaders::new(body.len() as u64);
99-
let proposal = UncheckedProposal::from_request(body, super::test::QUERY_PARAMS, headers)?;
99+
let proposal = UncheckedProposal::from_request(body, QUERY_PARAMS, headers)?;
100100

101101
let witness_utxo =
102102
proposal.psbt.inputs[0].witness_utxo.as_ref().expect("witness_utxo should be present");

payjoin/src/receive/v1/mod.rs

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,7 @@ impl WantsOutputs {
310310
&& txo.value < original_output.value
311311
{
312312
return Err(
313-
InternalOutputSubstitutionError::OutputSubstitutionDisabled(
314-
"Decreasing the receiver output value is not allowed",
315-
)
316-
.into(),
313+
InternalOutputSubstitutionError::DecreasedValueWhenDisabled.into(),
317314
);
318315
}
319316
outputs.push(txo);
@@ -322,10 +319,8 @@ impl WantsOutputs {
322319
None => {
323320
if self.params.disable_output_substitution {
324321
return Err(
325-
InternalOutputSubstitutionError::OutputSubstitutionDisabled(
326-
"Changing the receiver output script pubkey is not allowed",
327-
)
328-
.into(),
322+
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled
323+
.into(),
329324
);
330325
}
331326
let index = rng.gen_range(0..replacement_outputs.len());
@@ -788,20 +783,36 @@ pub(crate) mod test {
788783
use std::str::FromStr;
789784

790785
use bitcoin::{Address, Network};
791-
use payjoin_test_utils::ORIGINAL_PSBT;
786+
use payjoin_test_utils::{BoxError, ORIGINAL_PSBT, QUERY_PARAMS};
792787
use rand::rngs::StdRng;
793788
use rand::SeedableRng;
794789

795790
use super::*;
796-
pub const QUERY_PARAMS: &str = "maxadditionalfeecontribution=182&additionalfeeoutputindex=0";
797-
798-
pub(crate) fn proposal_from_test_vector(
799-
) -> Result<UncheckedProposal, Box<dyn std::error::Error>> {
791+
pub(crate) fn proposal_from_test_vector() -> Result<UncheckedProposal, BoxError> {
800792
let pairs = url::form_urlencoded::parse(QUERY_PARAMS.as_bytes());
801793
let params = Params::from_query_pairs(pairs, &[1])?;
802794
Ok(UncheckedProposal { psbt: bitcoin::Psbt::from_str(ORIGINAL_PSBT)?, params })
803795
}
804796

797+
fn wants_outputs_from_test_vector(
798+
proposal: UncheckedProposal,
799+
) -> Result<WantsOutputs, BoxError> {
800+
Ok(proposal
801+
.assume_interactive_receiver()
802+
.check_inputs_not_owned(|_| Ok(false))
803+
.expect("No inputs should be owned")
804+
.check_no_inputs_seen_before(|_| Ok(false))
805+
.expect("No inputs should be seen before")
806+
.identify_receiver_outputs(|script| {
807+
let network = Network::Bitcoin;
808+
Ok(Address::from_script(script, network).unwrap()
809+
== Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM")
810+
.unwrap()
811+
.require_network(network)
812+
.unwrap())
813+
})?)
814+
}
815+
805816
#[test]
806817
fn can_get_proposal_from_request() {
807818
let proposal = proposal_from_test_vector();
@@ -935,6 +946,75 @@ pub(crate) mod test {
935946
);
936947
}
937948

949+
#[test]
950+
fn test_pjos_disabled() {
951+
let mut proposal = proposal_from_test_vector().unwrap();
952+
// Specify outputsubstitution is disabled
953+
proposal.params.disable_output_substitution = true;
954+
let wants_outputs = wants_outputs_from_test_vector(proposal).unwrap();
955+
956+
let output_value =
957+
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value
958+
+ Amount::ONE_SAT;
959+
let outputs = vec![TxOut {
960+
value: output_value,
961+
script_pubkey: wants_outputs.original_psbt.unsigned_tx.output
962+
[wants_outputs.change_vout]
963+
.script_pubkey
964+
.clone(),
965+
}];
966+
let increased_amount = wants_outputs.clone().replace_receiver_outputs(
967+
outputs,
968+
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout]
969+
.script_pubkey
970+
.as_script(),
971+
);
972+
assert!(increased_amount.is_ok(), "Replacement Outputs should be a valid WantsOutput");
973+
assert_ne!(wants_outputs.payjoin_psbt, increased_amount.unwrap().payjoin_psbt);
974+
975+
let output_value =
976+
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value
977+
- Amount::ONE_SAT;
978+
let outputs = vec![TxOut {
979+
value: output_value,
980+
script_pubkey: wants_outputs.original_psbt.unsigned_tx.output
981+
[wants_outputs.change_vout]
982+
.script_pubkey
983+
.clone(),
984+
}];
985+
let decreased_amount = wants_outputs.clone().replace_receiver_outputs(
986+
outputs,
987+
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout]
988+
.script_pubkey
989+
.as_script(),
990+
);
991+
match decreased_amount {
992+
Ok(_) => panic!("Expected error, got success"),
993+
Err(error) => {
994+
assert_eq!(
995+
error,
996+
OutputSubstitutionError::from(
997+
InternalOutputSubstitutionError::DecreasedValueWhenDisabled
998+
)
999+
);
1000+
}
1001+
};
1002+
1003+
let script = Script::new();
1004+
let replace_receiver_script_pubkey = wants_outputs.substitute_receiver_script(script);
1005+
match replace_receiver_script_pubkey {
1006+
Ok(_) => panic!("Expected error, got success"),
1007+
Err(error) => {
1008+
assert_eq!(
1009+
error,
1010+
OutputSubstitutionError::from(
1011+
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled
1012+
)
1013+
);
1014+
}
1015+
};
1016+
}
1017+
9381018
#[test]
9391019
fn test_interleave_shuffle() {
9401020
let mut original1 = vec![1, 2, 3];

0 commit comments

Comments
 (0)