Async Interface for Payjoin Receiver State Machine#1546
Conversation
Coverage Report for CI Build 25603774925Coverage increased (+0.7%) to 85.992%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
|
I had some additional general questions, let me know if this should be posted in separate issue but if can be resolved here it would be nice to include potential fix ups in this PR:
Edit: please respond to this discussion in #1548 |
Hmm, good catch. Since this happens late in the state machine, it may make sense to do that on the fallback transaction when it is first received, but even then game theoretically that's the sender's concern not the receiver's, the receiver should be concerned with the absolute fees they pay or the feerate for their inputs/outputs in the transaction, less so with the payjoin transaction as a whole concerning fees that are too high which is what this method attempt to prevent. what's worse, this unconditionally calls Please open an issue even if fixing in this PR so that in case this PR gets bogged down in review we don't lose track of this bugfix
No
Yes, that's documented on That said, you point out a flaw in our design, which is that Please open an issue for this too, IMO that should be done in a separate PR since this is a conceptual change that is non-trivial
This seems unnecessary since that would be the receiver validating its own singuature, if a wallet is not able to produce valid signatures that's not the payjoin crate's responsibility to enforce |
spacebear21
left a comment
There was a problem hiding this comment.
Concept ACK. I think the approach can be simplified significantly by pre-computing async results and passing those as closures to the synchronous counterpart methods as explained below.
Also, 89310d8 could land separately in its own PR to expedite that fix and simplify review here.
| pub async fn check_broadcast_suitability_async<F>( | ||
| &self, | ||
| min_fee_rate: Option<FeeRate>, | ||
| can_broadcast: impl Fn(&bitcoin::Transaction) -> F, | ||
| ) -> Result<(), Error> | ||
| where | ||
| F: Future<Output = Result<bool, ImplementationError>>, | ||
| { | ||
| if let Some(min_fee_rate) = min_fee_rate { | ||
| self.check_min_fee_rate(min_fee_rate)? | ||
| } | ||
| if can_broadcast(&self.psbt.clone().extract_tx_unchecked_fee_rate()) | ||
| .await | ||
| .map_err(Error::Implementation)? | ||
| { | ||
| Ok(()) |
There was a problem hiding this comment.
I think we could pre-compute the result of the async callback and pass that to the synchronous check_broadcast_suitability.
we don't have to modify the latter at all, and check_broadcast_suitability_async becomes a two-liner. something like this:
pub async fn check_broadcast_suitability_async<F>(
&self,
min_fee_rate: Option<FeeRate>,
can_broadcast: impl Fn(&bitcoin::Transaction) -> F,
) -> Result<(), Error>
where
F: Future<Output = Result<bool, ImplementationError>>,
{
let can_broadcast = can_broadcast(&self.psbt.clone().extract_tx_unchecked_fee_rate())
.await
.map_err(Error::Implementation)?;
self.check_broadcast_suitability(min_fee_rate, |_| Ok(can_broadcast))
}The same pattern could apply to all other transition methods afaict.
| /// For the former, you should call [`Self::check_broadcast_suitability`] | ||
| /// or [`Self::check_broadcast_suitability_async`] to check that the proposal is actually |
There was a problem hiding this comment.
I don't think these docstrings really need updates, IMO it would be sufficient to simply add "Asynchronous counterpart to Self::check_broadcast_suitability" above the respective _async methods.
Summary
Currently some states (UncheckedOriginalPayload, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, ProvisionalProposal, Monitor) require the usage of synchronous callbacks to transition to the next state in the payjoin state machine. This is inconvenient for languages which required the use of asynchronous calls for validation, for example when calling the bitcoin rpc, which use the payjoin FFI language bindings. While there are some workarounds that can be used to adequately validate the state machine transitions (except for UncheckedOriginalPayload and Monitor) when relying on asynchronous validation, these are a bit unwieldily to use. Instead, this PR implements an asynchronous interface that makes state machine transitions with asynchronous calls possible.
This is an alternate version of #1446 . See rationale for reimplementing here.
Background
I was attempting to prove out the Javascript language bindings by implementing a Node version of the payjoin-cli and opened issue #1389 after encountering some of the challenges with using asynchronous validation callbacks. It was revealed to me that this was a known pain point and there was a #816 (comment) proposed by @arminsabouri of creating new state transition methods that accept the result of a validation call. This is my attempt at implementing this solution.
Side Effects
Some additional things that were touched in this PR:
psbt_to_signwas updated to strip out the sender signatures so that callers don't have to do this themselvesAny and all feedback is welcome!
AI Disclosure
Claude AI was used to write some of the tests and help with troubleshooting.
Please confirm the following before requesting review:
AI
in the body of this PR.