Skip to content

Commit 583c136

Browse files
committed
Add complete rust-payjoin src mutation coverage
This pr adds complete mutation coverage onto the payjoin crate within rust-payjoin. We did discover an outstanding mutant that we still want to explore more as to whether we should modify the source code or keep the behavior as is in #948.
1 parent ffeabdd commit 583c136

File tree

9 files changed

+203
-17
lines changed

9 files changed

+203
-17
lines changed

.cargo/mutants.toml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
additional_cargo_args = ["--all-features"]
22
gitignore = true
33
examine_globs = [
4-
"payjoin/src/core/ohttp.rs",
5-
"payjoin/src/core/uri/*.rs",
6-
"payjoin/src/core/receive/**/*.rs",
7-
"payjoin/src/core/send/**/*.rs"
4+
"payjoin/src/**/*.rs"
5+
]
6+
exclude_globs = [
87
]
9-
exclude_globs = []
108
exclude_re = [
11-
"impl Debug",
12-
"impl Display",
9+
"impl (fmt::)?(Debug|Display)",
1310
"deserialize",
1411
"Iterator",
1512
".*Error",
@@ -41,4 +38,5 @@ exclude_re = [
4138
"replace > with >= in WantsInputs::avoid_uih", # This mutation I am unsure about whether or not it is a trivial mutant and have not decided on how the best way to approach testing it is
4239
# payjoin/src/core/send/mod.rs
4340
"replace match guard proposed_txout.script_pubkey == original_output.script_pubkey with true in PsbtContext::check_outputs", # This non-deterministic mutation has a possible test to catch it
41+
"replace == with != in Receiver<Initialized>::unchecked_from_payload", #This mutant is something we intend to address in issue #948
4442
]

payjoin-cli/src/app/v1.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ impl App {
147147
"Listening at {}. Configured to accept payjoin at BIP 21 Payjoin Uri:",
148148
listener.local_addr()?
149149
);
150-
println!("{}", pj_uri_string);
150+
println!("{pj_uri_string}");
151151

152152
let app = self.clone();
153153

payjoin/src/core/hpke.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
use core::fmt;
2+
use std::error;
13
use std::ops::Deref;
2-
use std::{error, fmt};
34

45
use bitcoin::key::constants::{ELLSWIFT_ENCODING_SIZE, PUBLIC_KEY_SIZE};
56
use bitcoin::secp256k1;
@@ -88,7 +89,7 @@ impl Deref for HpkeSecretKey {
8889
fn deref(&self) -> &Self::Target { &self.0 }
8990
}
9091

91-
impl core::fmt::Debug for HpkeSecretKey {
92+
impl fmt::Debug for HpkeSecretKey {
9293
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9394
write!(f, "SecpHpkeSecretKey([REDACTED])")
9495
}
@@ -140,7 +141,7 @@ impl Deref for HpkePublicKey {
140141
fn deref(&self) -> &Self::Target { &self.0 }
141142
}
142143

143-
impl core::fmt::Debug for HpkePublicKey {
144+
impl fmt::Debug for HpkePublicKey {
144145
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145146
write!(f, "SecpHpkePublicKey({:?})", self.0)
146147
}

payjoin/src/core/into_url.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,27 @@ mod tests {
111111
let err = "blob:https://example.com".into_url().unwrap_err();
112112
assert_eq!(err.to_string(), "URL scheme is not allowed");
113113
}
114+
115+
#[test]
116+
fn into_url_conversions() {
117+
let input = "http://localhost/";
118+
let url = Url::parse(input).unwrap();
119+
120+
let url_ref = &url;
121+
assert_eq!(url_ref.as_str(), url.as_ref());
122+
assert_eq!(IntoUrlSealed::as_str(url_ref), url.as_ref());
123+
assert_eq!(IntoUrlSealed::as_str(&url_ref), url.as_ref());
124+
125+
let url_str: &str = input;
126+
assert_eq!(url_str, url.as_ref());
127+
assert_eq!(IntoUrlSealed::as_str(&url_str), url.as_ref());
128+
129+
let url_string: String = input.to_string();
130+
assert_eq!(url_string, url.as_ref());
131+
assert_eq!(IntoUrlSealed::as_str(&url_string), url.as_ref());
132+
133+
let url_string_ref: &String = &input.to_string();
134+
assert_eq!(url_string_ref, url.as_ref());
135+
assert_eq!(IntoUrlSealed::as_str(&url_string_ref), url.as_ref());
136+
}
114137
}

payjoin/src/core/persist.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt;
12
/// Handles cases where the transition either succeeds with a final result that ends the session, or hits a static condition and stays in the same state.
23
/// State transition may also be a fatal error or transient error.
34
pub struct MaybeSuccessTransitionWithNoResults<Event, SuccessValue, CurrentState, Err>(
@@ -263,7 +264,7 @@ pub struct RejectTransient<Err>(pub(crate) Err);
263264
/// The wrapper contains the error and should be returned to the caller.
264265
pub struct RejectBadInitInputs<Err>(Err);
265266

266-
impl<Err: std::error::Error> std::fmt::Display for RejectTransient<Err> {
267+
impl<Err: std::error::Error> fmt::Display for RejectTransient<Err> {
267268
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268269
let RejectTransient(err) = self;
269270
write!(f, "{err}")
@@ -327,7 +328,7 @@ impl<ApiError: std::error::Error, StorageError: std::error::Error> std::error::E
327328
{
328329
}
329330

330-
impl<ApiError: std::error::Error, StorageError: std::error::Error> std::fmt::Display
331+
impl<ApiError: std::error::Error, StorageError: std::error::Error> fmt::Display
331332
for PersistedError<ApiError, StorageError>
332333
{
333334
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -687,7 +688,7 @@ mod tests {
687688

688689
impl std::error::Error for InMemoryTestError {}
689690

690-
impl std::fmt::Display for InMemoryTestError {
691+
impl fmt::Display for InMemoryTestError {
691692
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692693
write!(f, "InMemoryTestError")
693694
}

payjoin/src/core/psbt/mod.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,121 @@ impl std::error::Error for InputWeightError {
404404
impl From<AddressTypeError> for InputWeightError {
405405
fn from(value: AddressTypeError) -> Self { Self::AddressType(value) }
406406
}
407+
408+
#[cfg(test)]
409+
mod test {
410+
use bitcoin::{Psbt, ScriptBuf, Transaction};
411+
use payjoin_test_utils::PARSED_ORIGINAL_PSBT;
412+
413+
use crate::psbt::{InputWeightError, InternalInputPair, InternalPsbtInputError, PsbtExt};
414+
415+
#[test]
416+
fn validate_input_utxos() {
417+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
418+
let validated_pairs = psbt.validate_input_utxos();
419+
assert!(validated_pairs.is_ok());
420+
}
421+
422+
#[test]
423+
fn input_pairs_validate_witness_utxo() {
424+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
425+
let txin = &psbt.unsigned_tx.input[0];
426+
let psbtin = &psbt.inputs[0];
427+
428+
let pair: InternalInputPair = InternalInputPair { txin, psbtin };
429+
assert!(pair.validate_utxo().is_ok());
430+
}
431+
432+
#[test]
433+
fn input_pairs_validate_non_witness_utxo() {
434+
// This test checks each variation of the validate_utxo match block with no
435+
// non_witness_utxo
436+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
437+
let raw_tx = "010000000001015721029046ec1840d5bc8f4e59ae8ac4b576191d5e7994c8d1c44ddeaffc176c0300000000fdffffff018e8d00000000000017a9144a87748bc7bcfee8290e36700eeca3112f53ecbe870140239d1975e0fc9b8345bce9a170a0224cf8eb327bfcaccf0f8b9434d17345579e4dcbb68f7be39eac7987dfaa08293b11fdc76ac28e26bd85e99a46b69675418100000000";
438+
439+
let mut txin = psbt.unsigned_tx.input[0].clone();
440+
let mut psbtin = psbt.inputs[0].clone();
441+
442+
let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex(raw_tx).unwrap();
443+
psbtin.non_witness_utxo = Some(transaction.clone());
444+
psbtin.witness_utxo = None;
445+
txin.previous_output.txid = transaction.compute_txid();
446+
447+
let pair: InternalInputPair = InternalInputPair { txin: &txin, psbtin: &psbtin };
448+
assert!(pair.validate_utxo().is_ok());
449+
}
450+
451+
#[test]
452+
fn input_pairs_unequal_txid() {
453+
// This test checks each variation of the validate_utxo match block where is it expected to
454+
// return an unequal_txid error
455+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
456+
let raw_tx = "010000000001015721029046ec1840d5bc8f4e59ae8ac4b576191d5e7994c8d1c44ddeaffc176c0300000000fdffffff018e8d00000000000017a9144a87748bc7bcfee8290e36700eeca3112f53ecbe870140239d1975e0fc9b8345bce9a170a0224cf8eb327bfcaccf0f8b9434d17345579e4dcbb68f7be39eac7987dfaa08293b11fdc76ac28e26bd85e99a46b69675418100000000";
457+
458+
let txin = &psbt.unsigned_tx.input[0];
459+
let mut psbtin = psbt.inputs[0].clone();
460+
461+
let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex(raw_tx).unwrap();
462+
psbtin.non_witness_utxo = Some(transaction);
463+
464+
let pair: InternalInputPair = InternalInputPair { txin, psbtin: &psbtin };
465+
let validated_utxo = pair.validate_utxo();
466+
assert_eq!(validated_utxo.unwrap_err(), InternalPsbtInputError::UnequalTxid);
467+
468+
let txin = &psbt.unsigned_tx.input[0];
469+
let mut psbtin = psbt.inputs[0].clone();
470+
471+
let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex(raw_tx).unwrap();
472+
psbtin.non_witness_utxo = Some(transaction);
473+
psbtin.witness_utxo = None;
474+
475+
let pair: InternalInputPair = InternalInputPair { txin, psbtin: &psbtin };
476+
let validated_utxo = pair.validate_utxo();
477+
assert_eq!(validated_utxo.unwrap_err(), InternalPsbtInputError::UnequalTxid);
478+
}
479+
480+
#[test]
481+
fn input_pairs_txout_mismatch() {
482+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
483+
let raw_tx = "010000000001015721029046ec1840d5bc8f4e59ae8ac4b576191d5e7994c8d1c44ddeaffc176c0300000000fdffffff018e8d00000000000017a9144a87748bc7bcfee8290e36700eeca3112f53ecbe870140239d1975e0fc9b8345bce9a170a0224cf8eb327bfcaccf0f8b9434d17345579e4dcbb68f7be39eac7987dfaa08293b11fdc76ac28e26bd85e99a46b69675418100000000";
484+
485+
let mut txin = psbt.unsigned_tx.input[0].clone();
486+
let mut psbtin = psbt.inputs[0].clone();
487+
488+
let transaction: Transaction = bitcoin::consensus::encode::deserialize_hex(raw_tx).unwrap();
489+
psbtin.non_witness_utxo = Some(transaction.clone());
490+
txin.previous_output.txid = transaction.compute_txid();
491+
492+
let pair: InternalInputPair = InternalInputPair { txin: &txin, psbtin: &psbtin };
493+
let validated_utxo = pair.validate_utxo();
494+
assert_eq!(validated_utxo.unwrap_err(), InternalPsbtInputError::SegWitTxOutMismatch);
495+
}
496+
497+
#[test]
498+
fn expected_input_weight() {
499+
let psbt: Psbt = PARSED_ORIGINAL_PSBT.clone();
500+
let txin = &psbt.unsigned_tx.input[0];
501+
let psbtin = psbt.inputs[0].clone();
502+
503+
let pair: InternalInputPair = InternalInputPair { txin, psbtin: &psbtin };
504+
let weight = pair.expected_input_weight();
505+
assert!(weight.is_ok());
506+
507+
let mut psbtin = psbt.inputs[0].clone();
508+
psbtin.final_script_sig = Some(
509+
ScriptBuf::from_hex(
510+
"22002065f91a53cb7120057db3d378bd0f7d944167d43a7dcbff15d6afc4823f1d3ed3",
511+
)
512+
.unwrap(),
513+
);
514+
let pair: InternalInputPair = InternalInputPair { txin, psbtin: &psbtin };
515+
let weight = pair.expected_input_weight();
516+
assert_eq!(weight.unwrap_err(), InputWeightError::NotSupported);
517+
518+
let mut psbtin = psbt.inputs[0].clone();
519+
psbtin.final_script_sig = None;
520+
let pair: InternalInputPair = InternalInputPair { txin, psbtin: &psbtin };
521+
let weight = pair.expected_input_weight();
522+
assert_eq!(weight.unwrap_err(), InputWeightError::NoRedeemScript)
523+
}
524+
}

payjoin/src/core/send/mod.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -730,16 +730,19 @@ impl V1Context {
730730

731731
#[cfg(test)]
732732
mod test {
733+
use std::collections::BTreeMap;
733734
use std::str::FromStr;
734735

735736
use bitcoin::absolute::LockTime;
736-
use bitcoin::bip32::{DerivationPath, Fingerprint};
737+
use bitcoin::bip32::{self, DerivationPath, Fingerprint};
737738
use bitcoin::ecdsa::Signature;
738739
use bitcoin::hex::FromHex;
740+
use bitcoin::psbt::raw::ProprietaryKey;
739741
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, SecretKey, SECP256K1};
740742
use bitcoin::taproot::TaprootBuilder;
741743
use bitcoin::{
742-
Amount, FeeRate, OutPoint, Script, ScriptBuf, Sequence, Witness, XOnlyPublicKey,
744+
psbt, Amount, FeeRate, NetworkKind, OutPoint, Script, ScriptBuf, Sequence, Witness,
745+
XOnlyPublicKey,
743746
};
744747
use payjoin_test_utils::{
745748
BoxError, PARSED_ORIGINAL_PSBT, PARSED_PAYJOIN_PROPOSAL,
@@ -1092,16 +1095,41 @@ mod test {
10921095
assert!(!proposal.inputs[0].bip32_derivation.is_empty());
10931096
assert!(proposal.outputs[0].tap_internal_key.is_some());
10941097
assert!(!proposal.outputs[0].bip32_derivation.is_empty());
1095-
let psbt_ctx = PsbtContextBuilder::new(proposal.clone(), payee, None)
1098+
let mut psbt_ctx = PsbtContextBuilder::new(proposal.clone(), payee, None)
10961099
.build(OutputSubstitution::Disabled)?;
10971100

1101+
let mut map = BTreeMap::new();
1102+
let secp = Secp256k1::new();
1103+
let seed = Vec::<u8>::from_hex("BEEFCAFE").unwrap();
1104+
let xpriv = bip32::Xpriv::new_master(NetworkKind::Main, &seed).unwrap();
1105+
let xpub: bip32::Xpub = bip32::Xpub::from_priv(&secp, &xpriv);
1106+
let value = (xpriv.fingerprint(&secp), DerivationPath::from_str("42'").unwrap());
1107+
map.insert(xpub, value);
1108+
psbt_ctx.original_psbt.xpub = map;
1109+
1110+
let mut map = BTreeMap::new();
1111+
let proprietary_key =
1112+
ProprietaryKey { prefix: b"mock_prefix".to_vec(), subtype: 0x00, key: vec![] };
1113+
let value = FromHex::from_hex("BEEFCAFE").unwrap();
1114+
map.insert(proprietary_key, value);
1115+
psbt_ctx.original_psbt.proprietary = map;
1116+
1117+
let mut map = BTreeMap::new();
1118+
let unknown_key: psbt::raw::Key = psbt::raw::Key { type_value: 0x00, key: vec![] };
1119+
let value = FromHex::from_hex("BEEFCAFE").unwrap();
1120+
map.insert(unknown_key, value);
1121+
psbt_ctx.original_psbt.unknown = map;
1122+
10981123
let body = create_v1_post_request(Url::from_str("HTTPS://EXAMPLE.COM/")?, psbt_ctx).0.body;
10991124
let res_str = std::str::from_utf8(&body)?;
11001125
let proposal = Psbt::from_str(res_str)?;
11011126
assert!(proposal.inputs[0].tap_internal_key.is_none());
11021127
assert!(proposal.inputs[0].bip32_derivation.is_empty());
11031128
assert!(proposal.outputs[0].tap_internal_key.is_none());
11041129
assert!(proposal.outputs[0].bip32_derivation.is_empty());
1130+
assert!(proposal.xpub.is_empty());
1131+
assert!(proposal.proprietary.is_empty());
1132+
assert!(proposal.unknown.is_empty());
11051133
Ok(())
11061134
}
11071135

payjoin/src/core/send/v1.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ mod test {
393393
}
394394
}
395395

396+
#[test]
397+
fn test_max_content_length() {
398+
assert_eq!(MAX_CONTENT_LENGTH, 4_000_000 * 4 / 3);
399+
}
400+
396401
#[test]
397402
fn test_non_witness_input_weight_const() {
398403
assert_eq!(NON_WITNESS_INPUT_WEIGHT, bitcoin::Weight::from_wu(160));

payjoin/src/directory.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,15 @@ impl std::str::FromStr for ShortId {
6969
(&bytes[..]).try_into()
7070
}
7171
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use crate::uri::ShortId;
76+
77+
#[test]
78+
fn short_id_conversion() {
79+
let short_id = ShortId([0; 8]);
80+
assert_eq!(short_id.as_bytes(), short_id.0);
81+
assert_eq!(short_id.as_slice(), short_id.0);
82+
}
83+
}

0 commit comments

Comments
 (0)