Skip to content

Commit e9a6d0c

Browse files
authored
Add more test coverage for receive v1 (payjoin#602)
Even though we are ultimately transitioning to focus on v2 making sure there is good test coverage on v1 can make sure to catch any regressions that may occur. v1 is at the core of our backwards-compatible state machines.
2 parents cf94655 + dbd4af6 commit e9a6d0c

File tree

6 files changed

+148
-113
lines changed

6 files changed

+148
-113
lines changed

.cargo/mutants.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
additional_cargo_args = ["--all-features"]
2-
examine_globs = ["payjoin/src/uri/*.rs"]
2+
examine_globs = ["payjoin/src/uri/*.rs", "payjoin/src/receive/v1/**/*.rs"]
33
exclude_globs = []
44
exclude_re = [
55
"impl Debug",

payjoin-test-utils/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ pub const ORIGINAL_PSBT: &str = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkx
272272

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

275+
/// Input contribution for the receiver, from the BIP78 test vector
276+
pub const RECEIVER_INPUT_CONTRIBUTION: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==";
277+
275278
pub static PARSED_ORIGINAL_PSBT: Lazy<Psbt> =
276279
Lazy::new(|| Psbt::from_str(ORIGINAL_PSBT).expect("known psbt should parse"));
277280

payjoin/src/receive/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,10 @@ impl std::error::Error for OutputSubstitutionError {
332332
///
333333
/// This is currently opaque type because we aren't sure which variants will stay.
334334
/// You can only display it.
335-
#[derive(Debug)]
335+
#[derive(Debug, PartialEq)]
336336
pub struct SelectionError(InternalSelectionError);
337337

338-
#[derive(Debug)]
338+
#[derive(Debug, PartialEq)]
339339
pub(crate) enum InternalSelectionError {
340340
/// No candidates available for selection
341341
Empty,

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ mod tests {
7373
use payjoin_test_utils::{ORIGINAL_PSBT, QUERY_PARAMS};
7474

7575
use super::*;
76+
77+
#[derive(Debug, Clone)]
7678
struct MockHeaders {
7779
length: String,
7880
}
@@ -91,10 +93,36 @@ mod tests {
9193
}
9294
}
9395

96+
#[test]
97+
fn test_parse_body() {
98+
let mut padded_body = ORIGINAL_PSBT.as_bytes().to_vec();
99+
assert_eq!(MAX_CONTENT_LENGTH, 5333333_usize);
100+
padded_body.resize(MAX_CONTENT_LENGTH + 1, 0);
101+
let headers = MockHeaders::new(padded_body.len() as u64);
102+
103+
let parsed_request = parse_body(headers.clone(), padded_body.as_slice());
104+
assert!(parsed_request.is_err());
105+
match parsed_request {
106+
Ok(_) => panic!("Expected error, got success"),
107+
Err(error) => {
108+
assert_eq!(
109+
error.to_string(),
110+
RequestError::from(InternalRequestError::ContentLengthTooLarge(
111+
padded_body.len()
112+
))
113+
.to_string()
114+
);
115+
}
116+
}
117+
}
118+
94119
#[test]
95120
fn test_from_request() -> Result<(), Box<dyn std::error::Error>> {
96121
let body = ORIGINAL_PSBT.as_bytes();
97122
let headers = MockHeaders::new(body.len() as u64);
123+
let parsed_request = parse_body(headers.clone(), body);
124+
assert!(parsed_request.is_ok());
125+
98126
let proposal = UncheckedProposal::from_request(body, QUERY_PARAMS, headers)?;
99127

100128
let witness_utxo =

payjoin/src/receive/v1/mod.rs

Lines changed: 113 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -775,21 +775,22 @@ pub(crate) mod test {
775775
use std::str::FromStr;
776776

777777
use bitcoin::{Address, Network};
778-
use payjoin_test_utils::{BoxError, ORIGINAL_PSBT, QUERY_PARAMS};
778+
use payjoin_test_utils::{PARSED_ORIGINAL_PSBT, QUERY_PARAMS, RECEIVER_INPUT_CONTRIBUTION};
779779
use rand::rngs::StdRng;
780780
use rand::SeedableRng;
781781

782782
use super::*;
783-
pub(crate) fn proposal_from_test_vector() -> Result<UncheckedProposal, BoxError> {
783+
use crate::receive::PayloadError;
784+
785+
pub(crate) fn unchecked_proposal_from_test_vector() -> UncheckedProposal {
784786
let pairs = url::form_urlencoded::parse(QUERY_PARAMS.as_bytes());
785-
let params = Params::from_query_pairs(pairs, &[1])?;
786-
Ok(UncheckedProposal { psbt: bitcoin::Psbt::from_str(ORIGINAL_PSBT)?, params })
787+
let params =
788+
Params::from_query_pairs(pairs, &[1]).expect("Could not parse params from query pairs");
789+
UncheckedProposal { psbt: PARSED_ORIGINAL_PSBT.clone(), params }
787790
}
788791

789-
fn wants_outputs_from_test_vector(
790-
proposal: UncheckedProposal,
791-
) -> Result<WantsOutputs, BoxError> {
792-
Ok(proposal
792+
fn wants_outputs_from_test_vector(proposal: UncheckedProposal) -> WantsOutputs {
793+
proposal
793794
.assume_interactive_receiver()
794795
.check_inputs_not_owned(|_| Ok(false))
795796
.expect("No inputs should be owned")
@@ -802,36 +803,46 @@ pub(crate) mod test {
802803
.unwrap()
803804
.require_network(network)
804805
.unwrap())
805-
})?)
806+
})
807+
.expect("Receiver output should be identified")
808+
}
809+
810+
fn provisional_proposal_from_test_vector(proposal: UncheckedProposal) -> ProvisionalProposal {
811+
wants_outputs_from_test_vector(proposal).commit_outputs().commit_inputs()
806812
}
807813

808814
#[test]
809-
fn can_get_proposal_from_request() {
810-
let proposal = proposal_from_test_vector();
811-
assert!(proposal.is_ok(), "OriginalPSBT should be a valid request");
815+
fn is_output_substitution_disabled() {
816+
let mut proposal = unchecked_proposal_from_test_vector();
817+
let payjoin = wants_outputs_from_test_vector(proposal.clone());
818+
assert_eq!(payjoin.output_substitution(), OutputSubstitution::Enabled);
819+
820+
proposal.params.output_substitution = OutputSubstitution::Disabled;
821+
let payjoin = wants_outputs_from_test_vector(proposal);
822+
assert_eq!(payjoin.output_substitution(), OutputSubstitution::Disabled);
823+
}
824+
825+
#[test]
826+
fn unchecked_proposal_below_min_fee() {
827+
let proposal = unchecked_proposal_from_test_vector();
828+
let min_fee_rate = FeeRate::MAX;
829+
match proposal.clone().check_broadcast_suitability(Some(min_fee_rate), |_| Ok(true)) {
830+
Err(ReplyableError::Payload(PayloadError(InternalPayloadError::PsbtBelowFeeRate(
831+
proposal_rate,
832+
min_rate,
833+
)))) => {
834+
assert_eq!(proposal_rate, proposal.clone().psbt_fee_rate().unwrap());
835+
assert_eq!(min_rate, min_fee_rate);
836+
},
837+
_ => panic!("Broadcast suitability check should fail due to being below the min fee rate or unexpected error type"),
838+
};
812839
}
813840

814841
#[test]
815842
fn unchecked_proposal_unlocks_after_checks() {
816-
let proposal = proposal_from_test_vector().unwrap();
843+
let proposal = unchecked_proposal_from_test_vector();
817844
assert_eq!(proposal.psbt_fee_rate().unwrap().to_sat_per_vb_floor(), 2);
818-
let payjoin = proposal
819-
.assume_interactive_receiver()
820-
.check_inputs_not_owned(|_| Ok(false))
821-
.expect("No inputs should be owned")
822-
.check_no_inputs_seen_before(|_| Ok(false))
823-
.expect("No inputs should be seen before")
824-
.identify_receiver_outputs(|script| {
825-
let network = Network::Bitcoin;
826-
Ok(Address::from_script(script, network).unwrap()
827-
== Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM")
828-
.unwrap()
829-
.require_network(network)
830-
.unwrap())
831-
})
832-
.expect("Receiver output should be identified")
833-
.commit_outputs()
834-
.commit_inputs();
845+
let payjoin = provisional_proposal_from_test_vector(proposal);
835846

836847
{
837848
let mut payjoin = payjoin.clone();
@@ -847,7 +858,7 @@ pub(crate) mod test {
847858

848859
#[test]
849860
fn empty_candidates_inputs() {
850-
let proposal = proposal_from_test_vector().unwrap();
861+
let proposal = unchecked_proposal_from_test_vector();
851862
let wants_inputs = proposal
852863
.assume_interactive_receiver()
853864
.check_inputs_not_owned(|_| Ok(false))
@@ -869,55 +880,47 @@ pub(crate) mod test {
869880
.commit_outputs();
870881
let empty_candidate_inputs: Vec<InputPair> = vec![];
871882
let result = wants_inputs.try_preserving_privacy(empty_candidate_inputs);
872-
match result {
873-
Err(err) => {
874-
let debug_str = format!("{:?}", err);
875-
assert!(
876-
debug_str.contains("Empty"),
877-
"Error should indicate 'Empty' but was: {}",
878-
debug_str
879-
);
880-
}
881-
Ok(_) => panic!("try_preserving_privacy should fail with empty candidate inputs"),
882-
}
883+
assert_eq!(
884+
result.unwrap_err(),
885+
SelectionError::from(InternalSelectionError::Empty),
886+
"try_preserving_privacy should fail with empty candidate inputs"
887+
);
883888
}
884889

885890
#[test]
886891
fn sender_specifies_excessive_fee_rate() {
887-
let mut proposal = proposal_from_test_vector().unwrap();
892+
let mut proposal = unchecked_proposal_from_test_vector();
888893
assert_eq!(proposal.psbt_fee_rate().unwrap().to_sat_per_vb_floor(), 2);
889894
// Specify excessive fee rate in sender params
890895
proposal.params.min_fee_rate = FeeRate::from_sat_per_vb_unchecked(1000);
891-
// Input contribution for the receiver, from the BIP78 test vector
892-
let proposal_psbt = Psbt::from_str("cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==").unwrap();
896+
let proposal_psbt = Psbt::from_str(RECEIVER_INPUT_CONTRIBUTION).unwrap();
893897
let input = InputPair {
894898
txin: proposal_psbt.unsigned_tx.input[1].clone(),
895899
psbtin: proposal_psbt.inputs[1].clone(),
896900
};
897-
let mut payjoin = proposal
898-
.assume_interactive_receiver()
899-
.check_inputs_not_owned(|_| Ok(false))
900-
.expect("No inputs should be owned")
901-
.check_no_inputs_seen_before(|_| Ok(false))
902-
.expect("No inputs should be seen before")
903-
.identify_receiver_outputs(|script| {
904-
let network = Network::Bitcoin;
905-
Ok(Address::from_script(script, network).unwrap()
906-
== Address::from_str("3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM")
907-
.unwrap()
908-
.require_network(network)
909-
.unwrap())
910-
})
911-
.expect("Receiver output should be identified")
901+
let mut payjoin = wants_outputs_from_test_vector(proposal)
912902
.commit_outputs()
913903
.contribute_inputs(vec![input])
914904
.expect("Failed to contribute inputs")
915905
.commit_inputs();
906+
let additional_output = TxOut {
907+
value: Amount::ZERO,
908+
script_pubkey: payjoin.original_psbt.unsigned_tx.output[0].script_pubkey.clone(),
909+
};
910+
payjoin.payjoin_psbt.unsigned_tx.output.push(additional_output);
916911
let mut payjoin_clone = payjoin.clone();
917912
let psbt = payjoin.apply_fee(None, Some(FeeRate::from_sat_per_vb_unchecked(1000)));
918913
assert!(psbt.is_ok(), "Payjoin should be a valid PSBT");
919914
let psbt = payjoin_clone.apply_fee(None, Some(FeeRate::from_sat_per_vb_unchecked(995)));
920-
assert!(psbt.is_err(), "Payjoin exceeds receiver fee preference and should error");
915+
match psbt {
916+
Err(InternalPayloadError::FeeTooHigh(proposed, max)) => {
917+
assert_eq!(FeeRate::from_str("249630").unwrap(), proposed);
918+
assert_eq!(FeeRate::from_sat_per_vb_unchecked(995), max);
919+
}
920+
_ => panic!(
921+
"Payjoin exceeds receiver fee preference and should error or unexpected error type"
922+
),
923+
}
921924
}
922925

923926
#[test]
@@ -977,70 +980,71 @@ pub(crate) mod test {
977980

978981
#[test]
979982
fn test_pjos_disabled() {
980-
let mut proposal = proposal_from_test_vector().unwrap();
983+
let mut proposal = unchecked_proposal_from_test_vector();
981984
proposal.params.output_substitution = OutputSubstitution::Disabled;
982-
let wants_outputs = wants_outputs_from_test_vector(proposal).unwrap();
985+
let wants_outputs = wants_outputs_from_test_vector(proposal);
986+
let script_pubkey = &wants_outputs.original_psbt.unsigned_tx.output
987+
[wants_outputs.change_vout]
988+
.script_pubkey;
983989

984990
let output_value =
985991
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value
986992
+ Amount::ONE_SAT;
987-
let outputs = vec![TxOut {
988-
value: output_value,
989-
script_pubkey: wants_outputs.original_psbt.unsigned_tx.output
990-
[wants_outputs.change_vout]
991-
.script_pubkey
992-
.clone(),
993-
}];
994-
let increased_amount = wants_outputs.clone().replace_receiver_outputs(
995-
outputs,
996-
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout]
997-
.script_pubkey
998-
.as_script(),
993+
let outputs = vec![TxOut { value: output_value, script_pubkey: script_pubkey.clone() }];
994+
let increased_amount =
995+
wants_outputs.clone().replace_receiver_outputs(outputs, script_pubkey.as_script());
996+
assert!(
997+
increased_amount.is_ok(),
998+
"Increasing the receiver output amount should always be allowed"
999999
);
1000-
assert!(increased_amount.is_ok(), "Replacement Outputs should be a valid WantsOutput");
10011000
assert_ne!(wants_outputs.payjoin_psbt, increased_amount.unwrap().payjoin_psbt);
10021001

10031002
let output_value =
10041003
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value
10051004
- Amount::ONE_SAT;
1006-
let outputs = vec![TxOut {
1007-
value: output_value,
1008-
script_pubkey: wants_outputs.original_psbt.unsigned_tx.output
1009-
[wants_outputs.change_vout]
1010-
.script_pubkey
1011-
.clone(),
1012-
}];
1013-
let decreased_amount = wants_outputs.clone().replace_receiver_outputs(
1014-
outputs,
1015-
wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout]
1016-
.script_pubkey
1017-
.as_script(),
1005+
let outputs = vec![TxOut { value: output_value, script_pubkey: script_pubkey.clone() }];
1006+
let decreased_amount =
1007+
wants_outputs.clone().replace_receiver_outputs(outputs, script_pubkey.as_script());
1008+
assert_eq!(
1009+
decreased_amount.unwrap_err(),
1010+
OutputSubstitutionError::from(
1011+
InternalOutputSubstitutionError::DecreasedValueWhenDisabled
1012+
),
1013+
"Payjoin receiver amount has been decreased and should error"
10181014
);
1019-
match decreased_amount {
1020-
Ok(_) => panic!("Expected error, got success"),
1021-
Err(error) => {
1022-
assert_eq!(
1023-
error,
1024-
OutputSubstitutionError::from(
1025-
InternalOutputSubstitutionError::DecreasedValueWhenDisabled
1026-
)
1027-
);
1028-
}
1029-
};
10301015

10311016
let script = Script::new();
10321017
let replace_receiver_script_pubkey = wants_outputs.substitute_receiver_script(script);
1033-
match replace_receiver_script_pubkey {
1034-
Ok(_) => panic!("Expected error, got success"),
1035-
Err(error) => {
1036-
assert_eq!(
1037-
error,
1038-
OutputSubstitutionError::from(
1039-
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled
1040-
)
1041-
);
1042-
}
1018+
assert_eq!(
1019+
replace_receiver_script_pubkey.unwrap_err(),
1020+
OutputSubstitutionError::from(
1021+
InternalOutputSubstitutionError::ScriptPubKeyChangedWhenDisabled
1022+
),
1023+
"Payjoin receiver script pubkey has been modified and should error"
1024+
);
1025+
}
1026+
1027+
#[test]
1028+
fn test_avoid_uih_one_output() {
1029+
let proposal = unchecked_proposal_from_test_vector();
1030+
let proposal_psbt = Psbt::from_str(RECEIVER_INPUT_CONTRIBUTION).unwrap();
1031+
let input = InputPair {
1032+
txin: proposal_psbt.unsigned_tx.input[1].clone(),
1033+
psbtin: proposal_psbt.inputs[1].clone(),
10431034
};
1035+
let input_iter = [input].into_iter();
1036+
let mut payjoin = wants_outputs_from_test_vector(proposal)
1037+
.commit_outputs()
1038+
.contribute_inputs(input_iter.clone())
1039+
.expect("Failed to contribute inputs");
1040+
1041+
payjoin.payjoin_psbt.outputs.pop();
1042+
let avoid_uih = payjoin.avoid_uih(input_iter);
1043+
assert_eq!(
1044+
avoid_uih.unwrap_err(),
1045+
SelectionError::from(InternalSelectionError::UnsupportedOutputLength),
1046+
"Payjoin below minimum allowed outputs for avoid uih and should error"
1047+
);
10441048
}
10451049

10461050
#[test]

payjoin/src/receive/v2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ mod test {
662662
#[test]
663663
fn extract_err_req() -> Result<(), BoxError> {
664664
let mut proposal = UncheckedProposal {
665-
v1: crate::receive::v1::test::proposal_from_test_vector()?,
665+
v1: crate::receive::v1::test::unchecked_proposal_from_test_vector(),
666666
context: SHARED_CONTEXT.clone(),
667667
};
668668

0 commit comments

Comments
 (0)