Skip to content

Conversation

@arturgontijo
Copy link
Contributor

@arturgontijo arturgontijo commented Jun 15, 2025

This PR implements a way to applications be able to set the weight of their inputs via expected_weight field of InputPair.
By doing that we will be able to solve #659

This PR also unblock the Liana's integration as it deals with P2wsh:
https://github.com/arturgontijo/liana/blob/payjoin-v3/lianad/src/payjoin/receiver.rs#L191-L197

TODO (if high level design is ok):

  • Proper set the commit message
  • Fix 2 broken tests (and improve existing ones)
  • Add tests

Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking this challenge on. Do you mind squashing your commits. And writing a commit message that captures the high level changes as well as relevant design decisions. In general Lint and Fmt fixes should be applied in the commit that introduces the issue. Thanks

@arturgontijo arturgontijo force-pushed the 659-round-II branch 2 times, most recently from 44fdd80 to 96c0b58 Compare June 15, 2025 16:16
@coveralls
Copy link
Collaborator

coveralls commented Jun 16, 2025

Pull Request Test Coverage Report for Build 16150204062

Details

  • 174 of 178 (97.75%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 85.571%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin/src/core/receive/mod.rs 60 61 98.36%
payjoin/src/core/psbt/mod.rs 1 4 25.0%
Totals Coverage Status
Change from base Build 16149447007: 0.2%
Covered Lines: 7514
Relevant Lines: 8781

💛 - Coveralls

Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a high-level the current design seems backwards.

I think InternalInputPair should remain mostly untouched. Instead, expected_weight should be added to the public-facing InputPair. When an InputPair is constructed for a predictable input type (p2pkh, p2wpkh...) we can use the expected_input_weight method to calculate the expected weight from the script and store that in InputPair.expected_weight. For p2sh/p2sh... the caller should specify expected_weight manually and it gets stored as-is. This way we shouldn't really need to change contribute_inputs except to use the expected_weight field instead of the expected_input_weight() method.

@arturgontijo arturgontijo force-pushed the 659-round-II branch 2 times, most recently from 9c90464 to fcee2cd Compare June 16, 2025 23:37
@arturgontijo
Copy link
Contributor Author

@0xBEEFCAF3 @spacebear21 ready to be reviewed again...

@arturgontijo arturgontijo changed the title Add contribute_inputs_with_weights() Add InputPair::new_with_weight() Jun 17, 2025
@DanGould DanGould force-pushed the master branch 2 times, most recently from 61351cb to 9f403f1 Compare June 20, 2025 20:17
@arturgontijo arturgontijo marked this pull request as draft June 20, 2025 23:02
@arturgontijo
Copy link
Contributor Author

Marking this as a draft as we still have things to discuss offline.

Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this forward. I had a few items. Main concern is unrelated changes.

@arturgontijo arturgontijo changed the title Add InputPair::new_with_weight() Add expected_weight field to InputPair Jun 24, 2025
@arturgontijo arturgontijo force-pushed the 659-round-II branch 2 times, most recently from 92ea08e to fb5bc60 Compare June 24, 2025 14:11
Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach ACK.

@arturgontijo arturgontijo force-pushed the 659-round-II branch 2 times, most recently from e570cd9 to f134955 Compare June 25, 2025 20:18
..Default::default()
};
InputPair::new(txin, psbtin).expect("Input pair should be valid")
InputPair::new(txin, psbtin, None).expect("Input pair should be valid")
Copy link
Contributor

@shinghim shinghim Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, and in other places where InputPair::new is called, wouldn't this cause an issue if this is a script (p2sh, p2wsh, p2tr) spend? From @spacebear21's comment in #583:

the unlocking script (i.e. signature) could be any arbitrary script of unpredictable weight and can't be known in advance of signing. Since wallets should have the information required to predict the max size of the eventual signature, we can have them provide an "max expected weight" or similar parameter when constructing InputPairs.

If we're just putting None here, we'd run into the same issue of just not having the expected weight field wouldn't we?

Copy link
Collaborator

@spacebear21 spacebear21 Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should probably switch to using the new_p2wpkh and other constructors for payjoin-cli. IIRC ListUnspentResultEntry has an address type field we could match on.

I think for payjoin-cli we could leave script spends unsupported for now since it's unlikely for bitcoin-cli users and not supported now anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InputPair::new() calls the expected_input_weight() (in the first place) that sets the expected_weight (if AddressType is supported), if that call breaks we then try to set the weight using the given expected_weight param (Option<>).
That said, wouldn't that cover the AddressType check?

Copy link
Contributor

@shinghim shinghim Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am i correct in assuming there's an objective/correct way to calculate the expected weight? If we're calling expected_input_weight in InputPair::new, we would hopefully be doing the calculation the correct way, and wouldn't need the user to provide the expected weight themselves, since they would just be doing what expected_input_weight is doing. If that's the case, i think it would make sense to keep the new field in InputPair, but populate it only through InputPair::new calling expected_input_weight internally. Then, the constructors would be more ergonomic, and the user wouldn't need to do duplicate work of calculating the weight themselves

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected_weight field (provided by users/applications) would be used in situations where the expected_input_weight() is not able to predict the weight of the given InputPair (the issue that this PR tries to solve points out to one of them: #659)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i see, the expected weight can only really be calculated for the key spends, and this is mostly for the script spends, which will just return an error if the user doesn't provide something, as those aren't supported in expected_input_weight()

assert_eq!(p2sh_pair.psbtin.non_witness_utxo.unwrap(), utxo);
InputPair::new_p2sh(utxo.clone(), outpoint, redeem_script.clone(), Some(sequence));

assert_eq!(
Copy link
Contributor Author

@arturgontijo arturgontijo Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spacebear21 @shinghim what do you guys recommend me to do here?
The issue is that while calling new() (from new_p2sh()) it fails in the match at:
https://github.com/payjoin/rust-payjoin/blob/master/payjoin/src/psbt/mod.rs#L203
As the given script is not a nested p2wpkh (tested by is_p2wpkh()).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the changes to InputPair::new, I think the script spends should have the additional field in their constructor to provide the expected weight. IIUC those are the types of inputs that actually need the user to provide them, since the key spends have a known max value (P2PKH_COMPRESSED_MAX/P2WPKH_MAX/NESTED_P2WPKH_MAX and maybe P2TR_KEY_DEFAULT_SIGHASH? though I'm not familiar enough with P2TR to know if we should return this value as the expected weight if the address is P2TR since there could also be a script path, right?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC we decided to solve the p2tr script path spends in a seperate PR. Documented here #788

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes i agree. Script path input constructors should have a param for weight.

@arturgontijo arturgontijo force-pushed the 659-round-II branch 3 times, most recently from 6581cb9 to 190ec3a Compare June 26, 2025 23:01
@arturgontijo
Copy link
Contributor Author

arturgontijo commented Jun 27, 2025

PR is now ready to get another round of reviews.
@0xBEEFCAF3 I used rebase+squash+new commit message, lmk if it's ok now.

Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is moving in the right direction. There a few smaller items. And request to catch duplicate input pairs for the reciver typestate field. One thing i don't love is that we have to create a dummy input_pair in InputPair::new(...). I think this can be resolved if we refactor expected_input_weight to be a pure function and not part of internal input pair. I can take a look into that

@arminsabouri arminsabouri force-pushed the 659-round-II branch 2 times, most recently from c6885eb to 9579130 Compare June 27, 2025 18:06
@arminsabouri
Copy link
Collaborator

Last push cleans up test_multiple_contribute_inputs() and resolves the duplicate input pair issue in contribute inputs

@arminsabouri
Copy link
Collaborator

Ended up reverting the dedup logic after noticing that we dont dedup inputs going into the payjoin psbt.

Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing i noticed. I will push a fix for this my self.

@arminsabouri arminsabouri force-pushed the 659-round-II branch 2 times, most recently from b706f7d to 4cef427 Compare June 30, 2025 17:00
pub fn new_p2wsh(
txout: TxOut,
outpoint: OutPoint,
witness_script: ScriptBuf,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to keep the witness_script parameter? IIRC it was only needed to calculate the expected weight but that's no longer relevant.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to keep the witness_script parameter?

Can't think of a good reason. I will remove it in a seperate commit on this PR.

Copy link
Collaborator

@arminsabouri arminsabouri Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing to note. Some wallets will require witness_script to be present in the psbt when signing and they may need to insert them later after they use the constructor. The same is true of key derivation fields as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I think the solution for fancier wallets will be to use the new_unchecked method and pass their own PsbtInput/TxIn. We'll just need to ensure those fields don't get cleared out elsewhere.

// P2wsh yet is not supported when expected weight is not provided
assert_eq!(
p2wsh_pair.err().unwrap(),
PsbtInputError::from(InternalPsbtInputError::from(InputWeightError::NotSupported))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me wonder if we should change this variant name since "NotSupported" implies it just needs to be implemented, but really this is more like "impossible to predict weight"

arturgontijo and others added 2 commits July 7, 2025 09:18
Co-authored-by: Armin Sabouri <armins88@gmail.com>

This change allows applications to specify the weight of their own inputs
via `InputPair::new()` method. This enables more
accurate fee estimation for complex inputs, such as P2WSH.

It solves issue payjoin#659.
Witness script was used to help derive the expected input weight. We now
require the application to provide the weight for variable weight inputs
(p2wsh).
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 311e775 - though there is a failing FFI check in CI.

@arminsabouri
Copy link
Collaborator

though there is a failing FFI check in CI.

Whoops missed that. Thanks
Last push should fix that https://github.com/payjoin/rust-payjoin/compare/311e775998d1ac8ab50f64c9d537a0b24347d9ff..92996ecc04e1cd06bf8d91966fc22c3b7cf50445

Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving so my req for change doesnt block merge status

@arminsabouri arminsabouri requested a review from spacebear21 July 8, 2025 17:58
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reACK 92996ec. Nice work!

@arminsabouri arminsabouri merged commit 26eff9b into payjoin:master Jul 8, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants