Add Payjoin Receiver Support (BIP 77) - #746
Camillarhi wants to merge 1 commit into
Conversation
|
👋 I see @tnull was un-assigned. |
1db2b08 to
49017bd
Compare
|
We've merged the async persistence PR you mentioned. You might want to build your draft PR on the merged commit from there on until we cut you a release. |
Thanks for letting me know. I'll build on the merged commit. |
8f6ba65 to
6499918
Compare
|
Are you stuck? Did something in our library break CI @Camillarhi |
Not stuck at all. I was just closing out some other PRs. Still working on this one, I'll let you know when it's ready. |
12a41ad to
eb97832
Compare
120b089 to
8cc2a31
Compare
5fe1a5c to
0411ada
Compare
|
Marking this as ready for review. The core receiver flow is implemented, including session persistence, PSBT handling, input contribution, mempool monitoring for payjoin transactions, and node restart recovery. Two things still pending that I'll follow up with:
Happy to get early feedback on the overall approach in the meantime. |
1 similar comment
1 similar comment
|
As mentioned elsewhere, we'll defer this to the 0.9 milestone. For now removed the review requests to silence the 5-fold notifications every day. Still ofc. intend to get back to this soon though. |
231c5bb to
5a9bf22
Compare
4802d17 to
eecb3ad
Compare
ca94538 to
f68d6f7
Compare
Implements the receiver side of the BIP 77 Payjoin v2 protocol, allowing LDK Node users to receive payjoin payments via a payjoin directory and OHTTP relay. - Adds a `PayjoinPayment` handler exposing a `receive()` method that returns a BIP 21 URI the sender can use to initiate the payjoin flow. The full receiver state machine is implemented covering all `ReceiveSession` states: polling the directory, validating the sender's proposal, contributing inputs, finalizing the PSBT, and monitoring the mempool. - Session state is persisted via `KVStorePayjoinReceiverPersister` and survives node restarts through event log replay. Sender inputs are tracked by `OutPoint` across polling attempts to prevent replay attacks. The sender's fallback transaction is broadcast on cancellation or failure to ensure the receiver still gets paid. - Adds `PaymentKind::Payjoin` to the payment store, `PayjoinConfig` for configuring the payjoin directory and OHTTP relay via `Builder::set_payjoin_config`, and background tasks for session resumption every 15 seconds and cleanup of terminal sessions after 24 hours.
f68d6f7 to
2d48b5e
Compare
|
@spacebear21 notified me out of band that version 1.0 has been released. This PR has been updated and now builds on the released 1.0.0. The 1.0 API moved a fair bit from the version the PR was originally based on, and I have handled all the changes it introduced. I also tested end-to-end on regtest against a Polar bitcoind, with payjoin-cli as the sender. @bc1cindy @spacebear21 @DanGould @zealsham, ready for another look since some changes have been made since the last review due to the version bump. |
|
I did a cursory review using Opus and Sol and found a couple larger design considerations that probably should be reworked before doing a low level review. There were also some straightforward implementation issues that I think are secondary to the design considerations. For now I think we should focus on resolving the design considerations before proceeding with cleaning up the implementation issues. With that being said, please let me know if there is anything here is inaccurate. Design considerationsPayjoin is not feature gatedI guess whether Payjoin should be feature gated is something that would require input from LDK team, however I would imagine that it probably should be. Even if the LDK team determines that Payjoin should not be feature gated, the current implementation breaks other currently supported features like cargo check --no-default-features --features uniffi-defaultit fails with errors. Bitcoin core is mandatory dependency (which is not explicitly enforced at build time)Currently Payjoin requires the Bitcoin core backend during the LDK node construction and throws an error if not found. This basically makes Bitcoin core a silent dependency of Payjoin that is only found at run time instead of build time when the Payjoin configuration exists. This feels like a bad anti-pattern. The main part of this concern is that nodes that rely on Esplora or Electrum backends cannot use Payjoin. Additionally, this also makes Payjoin unusable by the uniffi bindings (as mentioned in previous section). My suggestionThe best suggestion I can think of is to enable the
What do you think of this approach? I think the 2 main requirements that implementing Payjoin in LDK node should have (other than not breaking anything) are:
As for whether Payjoin should be feature gated I think I lean towards not feature gating it, or at least making it a default feature, however I would defer to LDK maintainers on that. Implementation detailsAnchor reserve not protected from being used in Payjoin txAnchor reserve UTXO can potentially be selected as an input to the Payjoin tx. Lack of UTXO lockingContributed UTXOs are never locked or reserved. A concurrent Mid-session errors strand the fallback for ~24hNon-transient Relay requests have no timeout and allow very large responsespost_request() uses a bare bitreq request: bitreq::post(req.url)
.with_header(...)
.with_body(...)
.send_async()See src/payment/payjoin/manager.rs:282-291. bitreq has no timeout by default and permits bodies up to 1 GiB by default. A stalled or hostile relay can hold a session task forever. Because resume_payjoin_sessions() waits for the whole JoinSet, one stuck session also prevents later 15-second resume rounds. These requests need a timeout, a small protocol-specific body limit, and explicit status validation. The scheduler does unbounded work every 15 secondsEvery cycle:
There is no session limit, concurrency limit, per-session backoff, or next-attempt timestamp. Applications can create unused URIs for 24 hours, making this an easy local resource exhaustion path. A per-session worker or persisted next-action schedule with bounded concurrency would fit better. The public API hides the session lifecyclereceive() returns only a URI: pub async fn receive(...) -> Result<String, Error>There is no public session ID, status query, cancellation method, or user-facing event. Background failures are only logged. The application cannot reliably correlate a URI with the resulting payment or distinguish an active session from a failed one. Returning a session handle containing the URI and ID would leave room for status() and cancel(). Disabling Payjoin strands existing sessionsThe manager is constructed only when config.payjoin_config is present. If a node issued Payjoin URIs and restarts without that config, its active persisted sessions are ignored. They will not be polled, expired, or closed with fallback. Existing active sessions should at least cause a warning or build error when Payjoin is disabled. |
This PR adds support for receiving payjoin payments in LDK Node. This is currently a work in progress and implements the receiver side of the payjoin protocol.
KVStorePayjoinReceiverPersisterto handle session persistencePayjoinas aPaymentKindto the payment storeNote on persistence: The payjoin library currently only supports synchronous persistence, but they're working on adding async support(payjoin/rust-payjoin#1235). This PR sets up the persistence structure (
KVStorePayjoinReceiverPersister), which will be updated to use async operations once the upstream PR is merged.This PR partially fixes #177 and fixes #1019