Skip to content

Commit 588fbdb

Browse files
committed
Implement PayjoinPayment ..
Payjoin [`BIP77`] implementation. Compatible with previous Payjoin version [`BIP78`]. Should be retrieved by calling [`Node::payjoin_payment`]. Payjoin transactions can be used to improve privacy by breaking the common-input-ownership heuristic when Payjoin receivers contribute input(s) to the transaction. They can also be used to save on fees, as the Payjoin receiver can direct the incoming funds to open a lightning channel, forwards the funds to another address, or simply consolidate UTXOs. In a Payjoin transaction, both the sender and receiver contribute inputs to the transaction in a coordinated manner. The Payjoin mechanism is also called pay-to-endpoint(P2EP). The Payjoin receiver endpoint address is communicated through a [`BIP21`] URI, along with the payment address and an optional amount parameter. In the Payjoin process, parties edit, sign and pass iterations of the transaction between each other, before a final version is broadcasted by the Payjoin sender. [`BIP77`] codifies a protocol with 2 iterations (or one round of interaction beyond address sharing). [`BIP77`] Defines the Payjoin process to happen asynchronously, with the Payjoin receiver enrolling with a Payjoin Directory to receive Payjoin requests. The Payjoin sender can then make requests through a proxy server, Payjoin Relay, to the Payjoin receiver even if the receiver is offline. This mechanism requires the Payjoin sender to regularly check for response from the Payjoin receiver as implemented in [`Node::payjoin_payment::send`]. A Payjoin Relay is a proxy server that forwards Payjoin requests from the Payjoin sender to the Payjoin receiver subdirectory. A Payjoin Relay can be run by anyone. Public Payjoin Relay servers are: - <https://pj.bobspacebkk.com> A Payjoin directory is a service that allows Payjoin receivers to receive Payjoin requests offline. A Payjoin directory can be run by anyone. Public Payjoin Directory servers are: - <https://payjo.in>
1 parent aa5c807 commit 588fbdb

File tree

9 files changed

+540
-2
lines changed

9 files changed

+540
-2
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
6969
esplora-client = { version = "0.6", default-features = false }
7070
libc = "0.2"
7171
uniffi = { version = "0.26.0", features = ["build"], optional = true }
72+
payjoin = { version = "0.16.0", default-features = false, features = ["send", "v2"] }
7273

7374
[target.'cfg(vss)'.dependencies]
7475
vss-client = "0.2"

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
4040
// The time in-between peer reconnection attempts.
4141
pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);
4242

43+
// The time before a payjoin http request is considered timed out.
44+
pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
45+
46+
// The duration between retries of a payjoin http request.
47+
pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);
48+
49+
// The total duration of retrying to send a payjoin http request.
50+
pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
51+
4352
// The time in-between RGS sync attempts.
4453
pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);
4554

src/error.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ pub enum Error {
9999
LiquiditySourceUnavailable,
100100
/// The given operation failed due to the LSP's required opening fee being too high.
101101
LiquidityFeeTooHigh,
102+
/// Failed to access Payjoin object.
103+
PayjoinUnavailable,
104+
/// Payjoin URI is invalid.
105+
PayjoinUriInvalid,
106+
/// Amount is neither user-provided nor defined in the URI.
107+
PayjoinRequestMissingAmount,
108+
/// Failed to build a Payjoin request.
109+
PayjoinRequestCreationFailed,
110+
/// Failed to send Payjoin request.
111+
PayjoinRequestSendingFailed,
112+
/// Payjoin response processing failed.
113+
PayjoinResponseProcessingFailed,
102114
}
103115

104116
impl fmt::Display for Error {
@@ -168,6 +180,30 @@ impl fmt::Display for Error {
168180
Self::LiquidityFeeTooHigh => {
169181
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
170182
},
183+
Self::PayjoinUnavailable => {
184+
write!(
185+
f,
186+
"Failed to access Payjoin object. Make sure you have enabled Payjoin support."
187+
)
188+
},
189+
Self::PayjoinRequestMissingAmount => {
190+
write!(
191+
f,
192+
"Amount is neither user-provided nor defined in the provided Payjoin URI."
193+
)
194+
},
195+
Self::PayjoinRequestCreationFailed => {
196+
write!(f, "Failed construct a Payjoin request")
197+
},
198+
Self::PayjoinUriInvalid => {
199+
write!(f, "The provided Payjoin URI is invalid")
200+
},
201+
Self::PayjoinRequestSendingFailed => {
202+
write!(f, "Failed to send Payjoin request")
203+
},
204+
Self::PayjoinResponseProcessingFailed => {
205+
write!(f, "Payjoin receiver responded to our request with an invalid response")
206+
},
171207
}
172208
}
173209
}

src/event.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,66 @@ pub enum Event {
142142
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
143143
reason: Option<ClosureReason>,
144144
},
145+
/// This event is emitted when we have successfully negotiated a Payjoin transaction with the
146+
/// receiver and are waiting for the transaction to be confirmed onchain.
147+
PayjoinPaymentAwaitingConfirmation {
148+
/// Transaction ID of the finalised Payjoin transaction. i.e., the final transaction after
149+
/// we have successfully negotiated with the receiver.
150+
txid: bitcoin::Txid,
151+
/// Transaction amount as specified in the Payjoin URI in case of using
152+
/// [`PayjoinPayment::send`] or as specified by the user if using
153+
/// [`PayjoinPayment::send_with_amount`].
154+
///
155+
/// [`PayjoinPayment::send`]: crate::PayjoinPayment::send
156+
/// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount
157+
amount_sats: u64,
158+
},
159+
/// This event is emitted when a Payjoin transaction has been successfully confirmed onchain.
160+
///
161+
/// This event is emitted only after one onchain confirmation. To determine the current number
162+
/// of confirmations, refer to [`PaymentStore::best_block`]:.
163+
///
164+
/// [`PaymentStore::best_block`]: crate::payment::store::PaymentStore::best_block
165+
PayjoinPaymentSuccessful {
166+
/// This can refer to the original PSBT or to the finalised Payjoin transaction.
167+
///
168+
/// If [`is_original_psbt_modified`] field is `true`, this refers to the finalised Payjoin
169+
/// transaction. Otherwise, it refers to the original PSBT.
170+
///
171+
/// In case of this being the original PSBT, the transaction will be a regular transaction
172+
/// and not a Payjoin transaction but will be considered successful as the receiver decided
173+
/// to broadcast the original PSBT or to respond with a Payjoin proposal that was identical
174+
/// to the original PSBT, and they have successfully received the funds.
175+
txid: bitcoin::Txid,
176+
/// Transaction amount as specified in the Payjoin URI in case of using
177+
/// [`PayjoinPayment::send`] or as specified by the user if using
178+
/// [`PayjoinPayment::send_with_amount`].
179+
///
180+
/// [`PayjoinPayment::send`]: crate::PayjoinPayment::send
181+
/// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount
182+
amount_sats: u64,
183+
/// Indicates whether the Payjoin negotiation was successful or the receiver decided to
184+
/// broadcast the original PSBT.
185+
is_original_psbt_modified: bool,
186+
},
187+
/// Failed to send a Payjoin transaction.
188+
///
189+
/// Payjoin payment can fail in different stages due to various reasons, such as network
190+
/// issues, insufficient funds, irresponsive receiver, etc.
191+
PayjoinPaymentFailed {
192+
/// This can refer to the original PSBT or to the finalised Payjoin transaction. Depending
193+
/// on the stage of the Payjoin process when the failure occurred.
194+
txid: bitcoin::Txid,
195+
/// Transaction amount as specified in the Payjoin URI in case of using
196+
/// [`PayjoinPayment::send`] or as specified by the user if using
197+
/// [`PayjoinPayment::send_with_amount`].
198+
///
199+
/// [`PayjoinPayment::send`]: crate::PayjoinPayment::send
200+
/// [`PayjoinPayment::send_with_amount`]: crate::PayjoinPayment::send_with_amount
201+
amount_sats: u64,
202+
/// Failure reason.
203+
reason: PayjoinPaymentFailureReason,
204+
},
145205
}
146206

147207
impl_writeable_tlv_based_enum!(Event,
@@ -183,9 +243,52 @@ impl_writeable_tlv_based_enum!(Event,
183243
(2, payment_id, required),
184244
(4, claimable_amount_msat, required),
185245
(6, claim_deadline, option),
246+
},
247+
(7, PayjoinPaymentAwaitingConfirmation) => {
248+
(0, txid, required),
249+
(2, amount_sats, required),
250+
},
251+
(9, PayjoinPaymentSuccessful) => {
252+
(0, txid, required),
253+
(2, amount_sats, required),
254+
(4, is_original_psbt_modified, required),
255+
},
256+
(10, PayjoinPaymentFailed) => {
257+
(0, amount_sats, required),
258+
(2, txid, required),
259+
(4, reason, required),
186260
};
187261
);
188262

263+
#[derive(Debug, Clone, PartialEq, Eq)]
264+
pub enum PayjoinPaymentFailureReason {
265+
/// The request failed in the sending process, i.e., either no funds were available to send or
266+
/// the provided Payjoin URI is invalid or network problem encountered while communicating with
267+
/// the Payjoin relay/directory. The exact reason can be determined by inspecting the logs.
268+
RequestSendingFailed,
269+
/// The received response was invalid, i.e., the receiver responded with an invalid Payjoin
270+
/// proposal that does not adhere to the [`BIP78`] specification.
271+
///
272+
/// This is considered a failure but the receiver can still broadcast the original PSBT, in
273+
/// which case a `PayjoinPaymentSuccessful` event will be emitted with
274+
/// `is_original_psbt_modified` set to `false` and the `txid` of the original PSBT.
275+
///
276+
/// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki
277+
ResponseProcessingFailed,
278+
/// The request failed as we did not receive a response in time.
279+
///
280+
/// This is considered a failure but the receiver can still broadcast the original PSBT, in
281+
/// which case a `PayjoinPaymentSuccessful` event will be emitted with
282+
/// `is_original_psbt_modified` set to `false` and the `txid` of the original PSBT.
283+
Timeout,
284+
}
285+
286+
impl_writeable_tlv_based_enum!(PayjoinPaymentFailureReason,
287+
(0, Timeout) => {},
288+
(1, RequestSendingFailed) => {},
289+
(2, ResponseProcessingFailed) => {};
290+
);
291+
189292
pub struct EventQueue<L: Deref>
190293
where
191294
L::Target: Logger,

src/payment/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
mod bolt11;
44
mod bolt12;
55
mod onchain;
6+
pub(crate) mod payjoin;
67
mod spontaneous;
78
pub(crate) mod store;
89
mod unified_qr;
910

11+
pub use self::payjoin::PayjoinPayment;
1012
pub use bolt11::Bolt11Payment;
1113
pub use bolt12::Bolt12Payment;
1214
pub use onchain::OnchainPayment;

src/payment/payjoin/handler.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use bitcoin::address::NetworkChecked;
2+
use bitcoin::psbt::Psbt;
3+
use bitcoin::{Script, Transaction, Txid};
4+
5+
use crate::config::PAYJOIN_REQUEST_TIMEOUT;
6+
use crate::error::Error;
7+
use crate::event::PayjoinPaymentFailureReason;
8+
use crate::logger::FilesystemLogger;
9+
use crate::payment::store::PaymentDetailsUpdate;
10+
use crate::payment::PaymentKind;
11+
use crate::payment::{PaymentDirection, PaymentStatus};
12+
use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet};
13+
use crate::Event;
14+
use crate::PaymentDetails;
15+
16+
use lightning::chain::Filter;
17+
use lightning::ln::channelmanager::PaymentId;
18+
use lightning::log_error;
19+
use lightning::util::logger::Logger;
20+
21+
use std::sync::Arc;
22+
23+
pub(crate) struct PayjoinHandler {
24+
chain_source: Arc<ChainSource>,
25+
event_queue: Arc<EventQueue>,
26+
logger: Arc<FilesystemLogger>,
27+
payjoin_relay: payjoin::Url,
28+
payment_store: Arc<PaymentStore>,
29+
wallet: Arc<Wallet>,
30+
}
31+
32+
impl PayjoinHandler {
33+
pub(crate) fn new(
34+
chain_source: Arc<ChainSource>, event_queue: Arc<EventQueue>,
35+
logger: Arc<FilesystemLogger>, payjoin_relay: payjoin::Url,
36+
payment_store: Arc<PaymentStore>, wallet: Arc<Wallet>,
37+
) -> Self {
38+
Self { chain_source, event_queue, logger, payjoin_relay, payment_store, wallet }
39+
}
40+
41+
pub(crate) fn start_request(
42+
&self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>,
43+
) -> Result<Psbt, Error> {
44+
let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?;
45+
let receiver = payjoin_uri.address.clone();
46+
let original_psbt =
47+
self.wallet.build_payjoin_transaction(amount, receiver.clone().into())?;
48+
let tx = original_psbt.clone().unsigned_tx;
49+
let payment_id = self.payment_id(&original_psbt.unsigned_tx.txid());
50+
self.payment_store.insert(PaymentDetails::new(
51+
payment_id,
52+
PaymentKind::Payjoin,
53+
Some(amount.to_sat()),
54+
PaymentDirection::Outbound,
55+
PaymentStatus::Pending,
56+
))?;
57+
let mut update_payment = PaymentDetailsUpdate::new(payment_id);
58+
update_payment.txid = Some(tx.txid());
59+
let _ = self.payment_store.update(&update_payment);
60+
self.chain_source.register_tx(&tx.txid(), Script::empty());
61+
Ok(original_psbt)
62+
}
63+
64+
pub(crate) async fn send_request(
65+
&self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt,
66+
) -> Result<Option<Psbt>, Error> {
67+
let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri(
68+
original_psbt.clone(),
69+
payjoin_uri.clone(),
70+
)
71+
.and_then(|b| b.build_non_incentivizing())
72+
.and_then(|mut c| c.extract_v2(self.payjoin_relay.clone()))
73+
.map_err(|e| {
74+
log_error!(self.logger, "Failed to create Payjoin request: {}", e);
75+
Error::PayjoinRequestCreationFailed
76+
})?;
77+
let mut headers = reqwest::header::HeaderMap::new();
78+
headers.insert(
79+
reqwest::header::CONTENT_TYPE,
80+
reqwest::header::HeaderValue::from_static("message/ohttp-req"),
81+
);
82+
let response = reqwest::Client::new()
83+
.post(request.url.clone())
84+
.body(request.body.clone())
85+
.timeout(PAYJOIN_REQUEST_TIMEOUT)
86+
.headers(headers)
87+
.send()
88+
.await
89+
.and_then(|r| r.error_for_status())
90+
.map_err(|e| {
91+
log_error!(self.logger, "Failed to send Payjoin request: {}", e);
92+
Error::PayjoinRequestSendingFailed
93+
})?;
94+
let response = response.bytes().await.map_err(|e| {
95+
log_error!(
96+
self.logger,
97+
"Failed to send Payjoin request, receiver invalid response: {}",
98+
e
99+
);
100+
Error::PayjoinRequestSendingFailed
101+
})?;
102+
let response = response.to_vec();
103+
context.process_response(&mut response.as_slice()).map_err(|e| {
104+
log_error!(self.logger, "Failed to process Payjoin response: {}", e);
105+
Error::PayjoinResponseProcessingFailed
106+
})
107+
}
108+
109+
pub(crate) fn process_response(
110+
&self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt,
111+
) -> Result<Transaction, Error> {
112+
let wallet = self.wallet.clone();
113+
wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?;
114+
let proposal_tx = payjoin_proposal.clone().extract_tx();
115+
let payment_store = self.payment_store.clone();
116+
let payment_id = self.payment_id(&original_psbt.unsigned_tx.txid());
117+
let payment_details = payment_store.get(&payment_id);
118+
if let Some(payment_details) = payment_details {
119+
let txid = proposal_tx.txid();
120+
let mut payment_update = PaymentDetailsUpdate::new(payment_id);
121+
payment_update.txid = Some(txid);
122+
payment_store.update(&payment_update)?;
123+
self.chain_source.register_tx(&txid, Script::empty());
124+
self.event_queue.add_event(Event::PayjoinPaymentAwaitingConfirmation {
125+
txid,
126+
amount_sats: payment_details
127+
.amount_msat
128+
.ok_or(Error::PayjoinRequestMissingAmount)?,
129+
})?;
130+
Ok(proposal_tx)
131+
} else {
132+
log_error!(self.logger, "Failed to process Payjoin response: transaction not found");
133+
Err(Error::PayjoinResponseProcessingFailed)
134+
}
135+
}
136+
137+
fn payment_id(&self, original_psbt_txid: &Txid) -> PaymentId {
138+
let payment_id: [u8; 32] =
139+
original_psbt_txid[..].try_into().expect("Unreachable, Txid is 32 bytes");
140+
PaymentId(payment_id)
141+
}
142+
143+
pub(crate) fn handle_request_failure(
144+
&self, original_psbt: &Psbt, reason: PayjoinPaymentFailureReason,
145+
) -> Result<(), Error> {
146+
let payment_store = self.payment_store.clone();
147+
let payment_id = &self.payment_id(&original_psbt.unsigned_tx.txid());
148+
let payment_details = payment_store.get(payment_id);
149+
if let Some(payment_details) = payment_details {
150+
let mut update_details = PaymentDetailsUpdate::new(payment_id.clone());
151+
update_details.status = Some(PaymentStatus::Failed);
152+
let _ = payment_store.update(&update_details);
153+
self.event_queue.add_event(Event::PayjoinPaymentFailed {
154+
txid: original_psbt.unsigned_tx.txid(),
155+
amount_sats: payment_details
156+
.amount_msat
157+
.ok_or(Error::PayjoinRequestMissingAmount)?,
158+
reason,
159+
})
160+
} else {
161+
log_error!(
162+
self.logger,
163+
"Failed to handle request failure for Payjoin payment: transaction not found"
164+
);
165+
Err(Error::PayjoinRequestSendingFailed)
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)