diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index b5ca50f9e..6f21de443 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -1,4 +1,5 @@ additional_cargo_args = ["--all-features"] +gitignore = true examine_globs = ["payjoin/src/core/uri/*.rs", "payjoin/src/core/receive/**/*.rs", "payjoin/src/core/send/**/*.rs"] exclude_globs = [] exclude_re = [ @@ -9,7 +10,32 @@ exclude_re = [ ".*Error", # ---------------------Crate-specific exculsions--------------------- - # Receive - # src/receive/v1/mod.rs + # Timeout loops + # src/receive/v1/mod.rs "interleave_shuffle", # Replacing index += 1 with index *= 1 in a loop causes a timeout due to an infinite loop + + # Trivial mutations + # These exlusions are allowing code blocks to run with artithmetic invloving zero and as a result do nothing + # payjoin/src/core/receive/v1/mod.rs + "replace > with >= in ProvisionalProposal::apply_fee", + # payjoin/src/core/send/mod.rs + "replace < with <= in PsbtContext::check_outputs", + "replace > with >= in PsbtContext::check_fees", + # payjoin/src/core/send/mod.rs + "replace < with <= in SenderBuilder<'a>::build_recommended", # clamping the fee contribution when the fee equals to the recommended fee does not do anything + + # Async SystemTime comparison + # checking if the system time is equal to the expiry is difficult to reasonably test + # payjoin/src/core/receive/v2/mod.rs + "replace < with <= in Receiver::apply_unchecked_from_payload", + "replace > with >= in Receiver::create_poll_request", + "replace > with >= in extract_err_req", + # payjoin/src/core/send/v2/mod.rs + "replace > with >= in Sender::create_v2_post_request", + + # TODO exclusions + # payjoin/src/core/receive/v1/mod.rs + "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 + # payjoin/src/core/send/mod.rs + "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 ] diff --git a/.github/workflows/cron-weekly-mutants.yml b/.github/workflows/cron-weekly-mutants.yml index 6d06bda5c..d18feefd9 100644 --- a/.github/workflows/cron-weekly-mutants.yml +++ b/.github/workflows/cron-weekly-mutants.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: taiki-e/install-action@v2 with: - tool: cargo-mutants@25.0.1 + tool: cargo-mutants@25.2.2 - run: cargo mutants --in-place --no-shuffle - uses: actions/upload-artifact@v4 if: always() diff --git a/payjoin/src/core/receive/multiparty/mod.rs b/payjoin/src/core/receive/multiparty/mod.rs index dbe718f8d..1bb3f7d08 100644 --- a/payjoin/src/core/receive/multiparty/mod.rs +++ b/payjoin/src/core/receive/multiparty/mod.rs @@ -365,6 +365,23 @@ mod test { Ok(()) } + #[test] + fn test_build_multiparty() -> Result<(), BoxError> { + let proposal_one = v2::UncheckedProposal { + v1: multiparty_proposals()[0].clone(), + context: SHARED_CONTEXT.clone(), + }; + let proposal_two = v2::UncheckedProposal { + v1: multiparty_proposals()[1].clone(), + context: SHARED_CONTEXT_TWO.clone(), + }; + let mut multiparty = UncheckedProposalBuilder::new(); + multiparty.add(v2::Receiver { state: proposal_one })?; + multiparty.add(v2::Receiver { state: proposal_two })?; + assert!(multiparty.build().is_ok()); + Ok(()) + } + #[test] fn test_duplicate_context_multiparty() -> Result<(), BoxError> { let proposal_one = v2::UncheckedProposal { diff --git a/payjoin/src/core/receive/v1/mod.rs b/payjoin/src/core/receive/v1/mod.rs index 52a07f167..10eb720b3 100644 --- a/payjoin/src/core/receive/v1/mod.rs +++ b/payjoin/src/core/receive/v1/mod.rs @@ -973,19 +973,25 @@ pub(crate) mod test { } #[test] - fn unchecked_proposal_below_min_fee() { + fn unchecked_proposal_min_fee() { let proposal = unchecked_proposal_from_test_vector(); + + let min_fee_rate = proposal.psbt_fee_rate().expect("Feerate calculation should not fail"); + let _ = proposal + .clone() + .check_broadcast_suitability(Some(min_fee_rate), |_| Ok(true)) + .expect("Broadcast suitability check with appropriate min_fee_rate should succeed"); + assert_eq!(proposal.clone().psbt_fee_rate().unwrap(), min_fee_rate); + let min_fee_rate = FeeRate::MAX; - match proposal.clone().check_broadcast_suitability(Some(min_fee_rate), |_| Ok(true)) { - Err(ReplyableError::Payload(PayloadError(InternalPayloadError::PsbtBelowFeeRate( - proposal_rate, - min_rate, - )))) => { - assert_eq!(proposal_rate, proposal.clone().psbt_fee_rate().unwrap()); - assert_eq!(min_rate, min_fee_rate); - }, - _ => panic!("Broadcast suitability check should fail due to being below the min fee rate or unexpected error type"), - }; + let expected_err = ReplyableError::Payload(PayloadError( + InternalPayloadError::PsbtBelowFeeRate(proposal.psbt_fee_rate().unwrap(), min_fee_rate), + )); + let proposal_below_min_fee = proposal + .clone() + .check_broadcast_suitability(Some(min_fee_rate), |_| Ok(true)) + .expect_err("Broadcast suitability with min_fee_rate below minimum should fail"); + assert_eq!(proposal_below_min_fee.to_string(), expected_err.to_string()); } #[test] @@ -1193,6 +1199,17 @@ pub(crate) mod test { [wants_outputs.change_vout] .script_pubkey; + let output_value = + wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value; + let outputs = vec![TxOut { value: output_value, script_pubkey: script_pubkey.clone() }]; + let unchanged_amount = + wants_outputs.clone().replace_receiver_outputs(outputs, script_pubkey.as_script()); + assert!( + unchanged_amount.is_ok(), + "Not touching the receiver output amount is always allowed" + ); + assert_ne!(wants_outputs.payjoin_psbt, unchanged_amount.unwrap().payjoin_psbt); + let output_value = wants_outputs.original_psbt.unsigned_tx.output[wants_outputs.change_vout].value + Amount::ONE_SAT; @@ -1201,7 +1218,7 @@ pub(crate) mod test { wants_outputs.clone().replace_receiver_outputs(outputs, script_pubkey.as_script()); assert!( increased_amount.is_ok(), - "Increasing the receiver output amount should always be allowed" + "Increasing the receiver output amount is always allowed" ); assert_ne!(wants_outputs.payjoin_psbt, increased_amount.unwrap().payjoin_psbt); diff --git a/payjoin/src/core/send/mod.rs b/payjoin/src/core/send/mod.rs index fdc0b45dc..64ed14695 100644 --- a/payjoin/src/core/send/mod.rs +++ b/payjoin/src/core/send/mod.rs @@ -772,6 +772,16 @@ mod test { fee_contribution.err(), Some(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution) ); + // This tests the max allowed fee contribution of the given input amount + let fee_contribution = determine_fee_contribution( + &PARSED_ORIGINAL_PSBT, + Script::from_bytes(& as FromHex>::from_hex( + "0014b60943f60c3ee848828bdace7474a92e81f3fcdd", + )?), + Some((Amount::from_sat(95983068), None)), + false, + ); + assert!(fee_contribution.is_ok()); Ok(()) } diff --git a/payjoin/src/core/send/v1.rs b/payjoin/src/core/send/v1.rs index aefe6d45f..b744a4f33 100644 --- a/payjoin/src/core/send/v1.rs +++ b/payjoin/src/core/send/v1.rs @@ -350,11 +350,14 @@ mod test { #[test] fn process_response_invalid_utf8() { - // In UTF-8, 0xF0 represents the start of a 4-byte sequence, so 0xF0 by itself is invalid - let invalid_utf8 = &[0xF0]; + // A PSBT expects an exact match so padding with null bytes for the from_str method is + // invalid + let mut invalid_utf8_padding = PAYJOIN_PROPOSAL.as_bytes().to_vec(); + invalid_utf8_padding + .extend(std::iter::repeat(0x00).take(MAX_CONTENT_LENGTH - invalid_utf8_padding.len())); let ctx = create_v1_context(); - let response = ctx.process_response(invalid_utf8); + let response = ctx.process_response(&invalid_utf8_padding); match response { Ok(_) => panic!("Invalid UTF-8 should have caused an error"), Err(error) => match error { @@ -389,4 +392,9 @@ mod test { }, } } + + #[test] + fn test_non_witness_input_weight_const() { + assert_eq!(NON_WITNESS_INPUT_WEIGHT, bitcoin::Weight::from_wu(160)); + } }