Conversation
|
I've assigned @tnull as a reviewer! |
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
1e1efcd to
5fe1a5c
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.
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. |
There was a problem hiding this comment.
hi there!
congrats and thanks for your work!
I talked to @spacebear21 and we agreed that I would review this PR, so I took a first look and here are some points:
needs to run cargo fmt --all
| "InvalidLnurl", | ||
| "PayjoinNotConfigured", | ||
| "PayjoinSessionCreationFailed", | ||
| "PayjoinSessionFailed" |
There was a problem hiding this comment.
PayjoinSessionFailed is used for infrastructure failures (coin selection, InputPair construction) and network errors (poll/post requests). The upstream lib distinguishes these via SelectionError, InputContributionError, and ReceiverError , would it be worth preserving that granularity so callers have more info and know how to act?
|
|
||
| pub(crate) async fn can_broadcast_transaction(&self, tx: &Transaction) -> Result<bool, Error> { | ||
| let timeout_fut = tokio::time::timeout( | ||
| Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), |
There was a problem hiding this comment.
since testmempoolaccept is local validation only, shouldn't this use DEFAULT_TX_LOOKUP_TIMEOUT_SECS?
| Err(e) => { | ||
| // Check if it's a "not found" error | ||
| let error_str = e.to_string(); | ||
| if error_str.contains("No such mempool or blockchain transaction") |
There was a problem hiding this comment.
matching errors by string seems fragile, as the message varies by server implementation.
e.g. tested: ssl://electrum.blockstream.info:60002: Protocol(String("missing transaction")) , which doesn't match either string here and Ok(None) would never be returned. the caller sees a sync failure instead
| self.runtime.spawn_blocking(move || electrum_client.transaction_get(&txid_copy)); | ||
| let timeout_fut = tokio::time::timeout( | ||
| Duration::from_secs( | ||
| self.sync_config.timeouts_config.lightning_wallet_sync_timeout_secs, |
There was a problem hiding this comment.
this is a single onchain tx lookup, should this use tx_broadcast_timeout_secs (10s) instead of lightning_wallet_sync_timeout_secs (30s)? or would a dedicated tx_lookup_timeout_secs field make more sense?
| &self, proposal: Receiver<Monitor>, persister: &KVStorePayjoinReceiverPersister, | ||
| ) -> Result<(), Error> { | ||
| // On a session resumption, the receiver will resume again in this state. | ||
| let poll_interval = tokio::time::Duration::from_millis(200); |
There was a problem hiding this comment.
this seems aggressive for a daemon (payjoin-cli uses 200ms because its a one-shot CLI with manual retry) but for ldk node maybe 1-5 seconds would be more appropriate?
| }) | ||
| .save_async(persister) | ||
| .await | ||
| .map_err(|_| Error::PersistenceFailed)?; |
There was a problem hiding this comment.
finalize_proposal returns MaybeTransientTransition to distinguish transient from fatal errors, but .map_err(|_| Error::PersistenceFailed) collapses both into the same error. Transient failures won't be retried and will surface as PersistenceFailed, which is misleading
| (4, secret, option), | ||
| } | ||
| }, | ||
| (11, Payjoin)=>{ |
There was a problem hiding this comment.
all other PaymentKind variants use even TLV tags (0, 2, 4, 6, 8, 10), was 11 intentional, or should this be 12?
| Arc::clone(&kv_store), | ||
| Arc::clone(&logger), | ||
| )), | ||
| Err(e) => { |
There was a problem hiding this comment.
payjoin_session_store_res doesn't handle ErrorKind::NotFound, which will occur on any node that has never used payjoin. fix suggestion in read_payjoin_sessions
| #bitcoin-payment-instructions = { version = "0.6" } | ||
| bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "869fd348c3ca0c78f439d2f31181f4d798c6b20e" } | ||
|
|
||
| payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", package = "payjoin", default-features = false, features = ["v2", "io"] } |
| PAYJOIN_SESSION_STORE_PRIMARY_NAMESPACE, | ||
| PAYJOIN_SESSION_STORE_SECONDARY_NAMESPACE, | ||
| ) | ||
| .await?; |
There was a problem hiding this comment.
list() returns NotFound when the namespace has never been written, maybe consider something like this handling it?
rust.or_else(|e| if e.kind() == lightning::io::ErrorKind::NotFound {
Ok(vec![])
} else {
Err(e)
})?;
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 will partially address #177