Fee rate check hardening - #1845
Conversation
Coverage Report for CI Build 33894569727Coverage increased (+0.1%) to 86.761%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
f01f0e4 to
2c25a0e
Compare
| // Determine the additional amount that the sender will pay in fees, | ||
| // clamped to the fee output's value so a malicious contribution | ||
| // cannot underflow the sender output |
There was a problem hiding this comment.
This seems like something that should have been validated earlier. We probably shouldn't even proceed with the payjoin if a sender is intentionally specifying a max fee contribution that they can't match
There was a problem hiding this comment.
I guess my only question is how do we do this check earlier? Don't we need all the receiver contributed inputs to be able to calculate this?
There was a problem hiding this comment.
i think the receiver should ignore any maxadditionalfeecontribution outside of the valid range [0;value of change output (+ dust value????)], and the sender should have an additional check when making the original PSBT that ensures their change output can fully cover maxadditionalfeecontribution
| let tx = self.psbt.clone().extract_tx_fee_rate_limit().map_err(|e| match e { | ||
| bitcoin::psbt::ExtractTxError::AbsurdFeeRate { fee_rate, .. } => | ||
| InternalPayloadError::OriginalPsbtAbsurdFee( | ||
| fee_rate, | ||
| bitcoin::Psbt::DEFAULT_MAX_FEE_RATE, | ||
| ), | ||
| _ => unreachable!("Input UTXOs validated and fee computed successfully"), | ||
| })?; | ||
| Ok(original_psbt_fee / tx.weight()) |
There was a problem hiding this comment.
What's the benefit of sanity checking the sender's fee rate here?
There was a problem hiding this comment.
I guess that is a pretty simple and cheap check and sort of addresses an adjecent version of your statement above We probably shouldn't even proceed with the payjoin if a sender is intentionally specifying a max fee contribution that they can't match keeping this at the OriginalPayload stage. In this case its not the max fee though but actually a selected fee by the sender
There was a problem hiding this comment.
I am also not sure if we need to do this sanity check here, as an absurdly high fee shouldn't affect anyone but the sender? In general I think a check like this should fall to the wallets to catch. Also I think this check is redundant because extract_tx_fee_rate_limit() seems to return ExtractTxError::AbsurdFeeRate by calling psbt.fee() under the hood and mapping the original Error::FeeOverflow error, but we would have already encountered said error because we already call psbt.fee() above.
If we do want to do this sanity check though I think it would be more fitting to put it into check_broadcast_suitability instead (since we are already checking for min fee rate there and intuitively seems like more appropriate place to check this). Or I guess we could also just map Error::FeeOverflow error to the OriginalPsbtAbsurdFee error when we call self.psbt.fee() above.
2c25a0e to
5cda85f
Compare
| let change_output = &mut payjoin_psbt.unsigned_tx.output[self.proposal.change_vout]; | ||
| change_output.value = | ||
| change_output.value.checked_sub(receiver_additional_fee).ok_or_else(|| { | ||
| InternalPayloadError::FeeTooHigh( |
There was a problem hiding this comment.
Maybe we should have a different error for this, since the issue isn't the fee being too high but the output being too low value? Don't feel strongly either way.
There was a problem hiding this comment.
Hmm, I tend to want to keep it simple here but I can see where you are coming from.
What do you think the error should be and what should it display?
OutputValueTooSmall but still compare feeRates?
There was a problem hiding this comment.
Yeah basically. And then maybe include the highest feasible fee for the output in the error enum? Since thats the only useful thing you can provide to the user to recover from this error at this state in the state machine.
But again, don't feel strongly on this one.
| let tx = self.psbt.clone().extract_tx_fee_rate_limit().map_err(|e| match e { | ||
| bitcoin::psbt::ExtractTxError::AbsurdFeeRate { fee_rate, .. } => | ||
| InternalPayloadError::OriginalPsbtAbsurdFee( | ||
| fee_rate, | ||
| bitcoin::Psbt::DEFAULT_MAX_FEE_RATE, | ||
| ), | ||
| _ => unreachable!("Input UTXOs validated and fee computed successfully"), | ||
| })?; | ||
| Ok(original_psbt_fee / tx.weight()) |
There was a problem hiding this comment.
I am also not sure if we need to do this sanity check here, as an absurdly high fee shouldn't affect anyone but the sender? In general I think a check like this should fall to the wallets to catch. Also I think this check is redundant because extract_tx_fee_rate_limit() seems to return ExtractTxError::AbsurdFeeRate by calling psbt.fee() under the hood and mapping the original Error::FeeOverflow error, but we would have already encountered said error because we already call psbt.fee() above.
If we do want to do this sanity check though I think it would be more fitting to put it into check_broadcast_suitability instead (since we are already checking for min fee rate there and intuitively seems like more appropriate place to check this). Or I guess we could also just map Error::FeeOverflow error to the OriginalPsbtAbsurdFee error when we call self.psbt.fee() above.
5cda85f to
35f6215
Compare
35f6215 to
221b9df
Compare
xstoicunicornx
left a comment
There was a problem hiding this comment.
Just one small new comment about accounting for dust when determining whether receiver output is of sufficient value.
| let change_output = &mut payjoin_psbt.unsigned_tx.output[self.proposal.change_vout]; | ||
| change_output.value = | ||
| change_output.value.checked_sub(receiver_additional_fee).ok_or_else(|| { | ||
| InternalPayloadError::FeeTooHigh( |
There was a problem hiding this comment.
Yeah basically. And then maybe include the highest feasible fee for the output in the error enum? Since thats the only useful thing you can provide to the user to recover from this error at this state in the state machine.
But again, don't feel strongly on this one.
| // sender minfeerate makes the fee exceed the change output's value. | ||
| let change_output = &mut payjoin_psbt.unsigned_tx.output[self.proposal.change_vout]; | ||
| change_output.value = | ||
| change_output.value.checked_sub(receiver_additional_fee).ok_or_else(|| { |
There was a problem hiding this comment.
| change_output.value.checked_sub(receiver_additional_fee).ok_or_else(|| { | |
| change_output.value.checked_sub(receiver_additional_fee + change_output.script_pubkey.minimal_non_dust()).ok_or_else(|| { |
Should we account for dust here?
This hardens the math on several areas of fee calculation. Namely this does checks that may occur in the bitcoin network but ensures that we do the checks early in the state machine ensuring that we can error out and either try again or close the session rather than accept a tx that will not succeed broadcasting.
Coded up with GLM-5.3
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.