Skip to content

Commit 26ecbb3

Browse files
committed
Allow users to specify the PaymentId used in InvoicePayer
In order to allow users to pass a custom idempotency key to the `send*` methods in `InvoicePayer`, we have to pipe the `PaymentId` through to the `Payer` methods, which we do here. By default, existing `InvoicePayer` methods use the `PaymentHash` as the `PaymentId`, however we also add duplicate `send*_with_id` methods which allow users to pass a custom `PaymentId`. Finally, appropriate documentation updates are made to clarify idempotency guarantees.
1 parent 120dafc commit 26ecbb3

File tree

2 files changed

+121
-44
lines changed

2 files changed

+121
-44
lines changed

lightning-invoice/src/payment.rs

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@
5959
//! # fn node_id(&self) -> PublicKey { unimplemented!() }
6060
//! # fn first_hops(&self) -> Vec<ChannelDetails> { unimplemented!() }
6161
//! # fn send_payment(
62-
//! # &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>
63-
//! # ) -> Result<PaymentId, PaymentSendFailure> { unimplemented!() }
62+
//! # &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
63+
//! # payment_id: PaymentId
64+
//! # ) -> Result<(), PaymentSendFailure> { unimplemented!() }
6465
//! # fn send_spontaneous_payment(
65-
//! # &self, route: &Route, payment_preimage: PaymentPreimage
66-
//! # ) -> Result<PaymentId, PaymentSendFailure> { unimplemented!() }
66+
//! # &self, route: &Route, payment_preimage: PaymentPreimage, payment_id: PaymentId,
67+
//! # ) -> Result<(), PaymentSendFailure> { unimplemented!() }
6768
//! # fn retry_payment(
6869
//! # &self, route: &Route, payment_id: PaymentId
6970
//! # ) -> Result<(), PaymentSendFailure> { unimplemented!() }
@@ -242,6 +243,19 @@ impl<T: Time> Display for PaymentAttempts<T> {
242243
}
243244

244245
/// A trait defining behavior of an [`Invoice`] payer.
246+
///
247+
/// While the behavior of [`InvoicePayer`] provides idempotency of duplicate `send_*payment` calls
248+
/// with the same [`PaymentHash`], it is up to the `Payer` to provide idempotency across restarts.
249+
///
250+
/// The lightning [`ChannelManager`] provides idempotency for duplicate payments with the same
251+
/// [`PaymentId`].
252+
///
253+
/// In order to trivially ensure idempotency for payments, the default `Payer` implementation
254+
/// reuses the [`PaymentHash`] bytes as the [`PaymentId`]. Custom implementations wishing to
255+
/// provide payment idempotency with a different idempotency key (i.e. [`PaymentId`]) should map
256+
/// the [`Invoice`] or spontaneous payment target pubkey to their own idempotency key.
257+
///
258+
/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager
245259
pub trait Payer {
246260
/// Returns the payer's node id.
247261
fn node_id(&self) -> PublicKey;
@@ -251,13 +265,14 @@ pub trait Payer {
251265

252266
/// Sends a payment over the Lightning Network using the given [`Route`].
253267
fn send_payment(
254-
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>
255-
) -> Result<PaymentId, PaymentSendFailure>;
268+
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
269+
payment_id: PaymentId
270+
) -> Result<(), PaymentSendFailure>;
256271

257272
/// Sends a spontaneous payment over the Lightning Network using the given [`Route`].
258273
fn send_spontaneous_payment(
259-
&self, route: &Route, payment_preimage: PaymentPreimage
260-
) -> Result<PaymentId, PaymentSendFailure>;
274+
&self, route: &Route, payment_preimage: PaymentPreimage, payment_id: PaymentId
275+
) -> Result<(), PaymentSendFailure>;
261276

262277
/// Retries a failed payment path for the [`PaymentId`] using the given [`Route`].
263278
fn retry_payment(&self, route: &Route, payment_id: PaymentId) -> Result<(), PaymentSendFailure>;
@@ -346,36 +361,74 @@ where
346361

347362
/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
348363
///
349-
/// You should ensure that the `invoice.payment_hash()` is unique and the same payment_hash has
350-
/// never been paid before. Because [`InvoicePayer`] is stateless no effort is made to do so
351-
/// for you.
364+
/// `invoice.payment_hash()` is used as the [`PaymentId`], which ensures idempotency as long as
365+
/// the payment is still pending. Once the payment completes or fails, you must ensure that a
366+
/// second payment with the same [`PaymentHash`] is never sent.
352367
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {
353368
if invoice.amount_milli_satoshis().is_none() {
354369
Err(PaymentError::Invoice("amount missing"))
355370
} else {
356-
self.pay_invoice_using_amount(invoice, None)
371+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
372+
self.pay_invoice_using_amount(invoice, None, payment_id).map(|()| payment_id)
373+
}
374+
}
375+
376+
/// Pays the given [`Invoice`] with a custom idempotency key, caching the invoice for later use
377+
/// in case a retry is needed.
378+
///
379+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
380+
/// payment completes or fails, you must ensure that a second payment with the same
381+
/// [`PaymentId`] is never sent.
382+
///
383+
/// You should ensure that the `invoice.payment_hash()` is unique and the same [`PaymentHash`]
384+
/// has never been paid before.
385+
pub fn pay_invoice_with_id(&self, invoice: &Invoice, payment_id: PaymentId) -> Result<(), PaymentError> {
386+
if invoice.amount_milli_satoshis().is_none() {
387+
Err(PaymentError::Invoice("amount missing"))
388+
} else {
389+
self.pay_invoice_using_amount(invoice, None, payment_id)
357390
}
358391
}
359392

360393
/// Pays the given zero-value [`Invoice`] using the given amount, caching it for later use in
361394
/// case a retry is needed.
362395
///
363-
/// You should ensure that the `invoice.payment_hash()` is unique and the same payment_hash has
364-
/// never been paid before. Because [`InvoicePayer`] is stateless no effort is made to do so
365-
/// for you.
396+
/// `invoice.payment_hash()` is used as the [`PaymentId`], which ensures idempotency as long as
397+
/// the payment is still pending. Once the payment completes or fails, you must ensure that a
398+
/// second payment with the same [`PaymentHash`] is never sent.
366399
pub fn pay_zero_value_invoice(
367400
&self, invoice: &Invoice, amount_msats: u64
368401
) -> Result<PaymentId, PaymentError> {
369402
if invoice.amount_milli_satoshis().is_some() {
370403
Err(PaymentError::Invoice("amount unexpected"))
371404
} else {
372-
self.pay_invoice_using_amount(invoice, Some(amount_msats))
405+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
406+
self.pay_invoice_using_amount(invoice, Some(amount_msats), payment_id).map(|()| payment_id)
407+
}
408+
}
409+
410+
/// Pays the given zero-value [`Invoice`] using the given amount and custom idempotency key,
411+
/// caching the invoice for later use in case a retry is needed.
412+
///
413+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
414+
/// payment completes or fails, you must ensure that a second payment with the same
415+
/// [`PaymentId`] is never sent.
416+
///
417+
/// You should ensure that the `invoice.payment_hash()` is unique and the same [`PaymentHash`]
418+
/// has never been paid before.
419+
pub fn pay_zero_value_invoice_with_id(
420+
&self, invoice: &Invoice, amount_msats: u64, payment_id: PaymentId
421+
) -> Result<(), PaymentError> {
422+
if invoice.amount_milli_satoshis().is_some() {
423+
Err(PaymentError::Invoice("amount unexpected"))
424+
} else {
425+
self.pay_invoice_using_amount(invoice, Some(amount_msats), payment_id)
373426
}
374427
}
375428

376429
fn pay_invoice_using_amount(
377-
&self, invoice: &Invoice, amount_msats: Option<u64>
378-
) -> Result<PaymentId, PaymentError> {
430+
&self, invoice: &Invoice, amount_msats: Option<u64>, payment_id: PaymentId
431+
) -> Result<(), PaymentError> {
379432
debug_assert!(invoice.amount_milli_satoshis().is_some() ^ amount_msats.is_some());
380433

381434
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
@@ -398,7 +451,7 @@ where
398451
};
399452

400453
let send_payment = |route: &Route| {
401-
self.payer.send_payment(route, payment_hash, &payment_secret)
454+
self.payer.send_payment(route, payment_hash, &payment_secret, payment_id)
402455
};
403456

404457
self.pay_internal(&route_params, payment_hash, send_payment)
@@ -408,13 +461,39 @@ where
408461
/// Pays `pubkey` an amount using the hash of the given preimage, caching it for later use in
409462
/// case a retry is needed.
410463
///
411-
/// You should ensure that `payment_preimage` is unique and that its `payment_hash` has never
412-
/// been paid before. Because [`InvoicePayer`] is stateless no effort is made to do so for you.
464+
/// The hash of the [`PaymentPreimage`] is used as the [`PaymentId`], which ensures idempotency
465+
/// as long as the payment is still pending. Once the payment completes or fails, you must
466+
/// ensure that a second payment with the same [`PaymentPreimage`] is never sent.
413467
pub fn pay_pubkey(
414468
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, amount_msats: u64,
415469
final_cltv_expiry_delta: u32
416470
) -> Result<PaymentId, PaymentError> {
417471
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
472+
let payment_id = PaymentId(payment_hash.0);
473+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
474+
final_cltv_expiry_delta)
475+
.map(|()| payment_id)
476+
}
477+
478+
/// Pays `pubkey` an amount using the hash of the given preimage, caching it for later use in
479+
/// case a retry is needed.
480+
///
481+
/// The hash of the [`PaymentPreimage`] is used as the [`PaymentId`], which ensures idempotency
482+
/// as long as the payment is still pending. Once the payment completes or fails, you must
483+
/// ensure that a second payment with the same [`PaymentPreimage`] is never sent.
484+
pub fn pay_pubkey_with_id(
485+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_id: PaymentId,
486+
amount_msats: u64, final_cltv_expiry_delta: u32
487+
) -> Result<(), PaymentError> {
488+
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
489+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
490+
final_cltv_expiry_delta)
491+
}
492+
493+
fn do_pay_pubkey(
494+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_hash: PaymentHash,
495+
payment_id: PaymentId, amount_msats: u64, final_cltv_expiry_delta: u32
496+
) -> Result<(), PaymentError> {
418497
match self.payment_cache.lock().unwrap().entry(payment_hash) {
419498
hash_map::Entry::Occupied(_) => return Err(PaymentError::Invoice("payment pending")),
420499
hash_map::Entry::Vacant(entry) => entry.insert(PaymentInfo::new()),
@@ -427,15 +506,15 @@ where
427506
};
428507

429508
let send_payment = |route: &Route| {
430-
self.payer.send_spontaneous_payment(route, payment_preimage)
509+
self.payer.send_spontaneous_payment(route, payment_preimage, payment_id)
431510
};
432511
self.pay_internal(&route_params, payment_hash, send_payment)
433512
.map_err(|e| { self.payment_cache.lock().unwrap().remove(&payment_hash); e })
434513
}
435514

436-
fn pay_internal<F: FnOnce(&Route) -> Result<PaymentId, PaymentSendFailure> + Copy>(
515+
fn pay_internal<F: FnOnce(&Route) -> Result<(), PaymentSendFailure> + Copy>(
437516
&self, params: &RouteParameters, payment_hash: PaymentHash, send_payment: F,
438-
) -> Result<PaymentId, PaymentError> {
517+
) -> Result<(), PaymentError> {
439518
#[cfg(feature = "std")] {
440519
if has_expired(params) {
441520
log_trace!(self.logger, "Invoice expired prior to send for payment {}", log_bytes!(payment_hash.0));
@@ -452,11 +531,11 @@ where
452531
).map_err(|e| PaymentError::Routing(e))?;
453532

454533
match send_payment(&route) {
455-
Ok(payment_id) => {
534+
Ok(()) => {
456535
for path in route.paths {
457536
self.process_path_inflight_htlcs(payment_hash, path);
458537
}
459-
Ok(payment_id)
538+
Ok(())
460539
},
461540
Err(e) => match e {
462541
PaymentSendFailure::ParameterError(_) => Err(e),
@@ -491,13 +570,13 @@ where
491570
// consider the payment sent, so return `Ok()` here, ignoring any retry
492571
// errors.
493572
let _ = self.retry_payment(payment_id, payment_hash, &retry_data);
494-
Ok(payment_id)
573+
Ok(())
495574
} else {
496575
// This may happen if we send a payment and some paths fail, but
497576
// only due to a temporary monitor failure or the like, implying
498577
// they're really in-flight, but we haven't sent the initial
499578
// HTLC-Add messages yet.
500-
Ok(payment_id)
579+
Ok(())
501580
}
502581
},
503582
},
@@ -2056,13 +2135,13 @@ mod tests {
20562135
self
20572136
}
20582137

2059-
fn check_attempts(&self) -> Result<PaymentId, PaymentSendFailure> {
2138+
fn check_attempts(&self) -> Result<(), PaymentSendFailure> {
20602139
let mut attempts = self.attempts.borrow_mut();
20612140
*attempts += 1;
20622141

20632142
match self.failing_on_attempt.borrow_mut().remove(&*attempts) {
20642143
Some(failure) => Err(failure),
2065-
None => Ok(PaymentId([1; 32])),
2144+
None => Ok(())
20662145
}
20672146
}
20682147

@@ -2100,15 +2179,15 @@ mod tests {
21002179

21012180
fn send_payment(
21022181
&self, route: &Route, _payment_hash: PaymentHash,
2103-
_payment_secret: &Option<PaymentSecret>
2104-
) -> Result<PaymentId, PaymentSendFailure> {
2182+
_payment_secret: &Option<PaymentSecret>, _payment_id: PaymentId,
2183+
) -> Result<(), PaymentSendFailure> {
21052184
self.check_value_msats(Amount::ForInvoice(route.get_total_amount()));
21062185
self.check_attempts()
21072186
}
21082187

21092188
fn send_spontaneous_payment(
2110-
&self, route: &Route, _payment_preimage: PaymentPreimage,
2111-
) -> Result<PaymentId, PaymentSendFailure> {
2189+
&self, route: &Route, _payment_preimage: PaymentPreimage, _payment_id: PaymentId,
2190+
) -> Result<(), PaymentSendFailure> {
21122191
self.check_value_msats(Amount::Spontaneous(route.get_total_amount()));
21132192
self.check_attempts()
21142193
}
@@ -2117,7 +2196,7 @@ mod tests {
21172196
&self, route: &Route, _payment_id: PaymentId
21182197
) -> Result<(), PaymentSendFailure> {
21192198
self.check_value_msats(Amount::OnRetry(route.get_total_amount()));
2120-
self.check_attempts().map(|_| ())
2199+
self.check_attempts()
21212200
}
21222201

21232202
fn abandon_payment(&self, _payment_id: PaymentId) { }

lightning-invoice/src/utils.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -517,18 +517,16 @@ where
517517
}
518518

519519
fn send_payment(
520-
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>
521-
) -> Result<PaymentId, PaymentSendFailure> {
522-
let payment_id = PaymentId(payment_hash.0);
523-
self.send_payment(route, payment_hash, payment_secret, payment_id).map(|()| payment_id)
520+
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
521+
payment_id: PaymentId
522+
) -> Result<(), PaymentSendFailure> {
523+
self.send_payment(route, payment_hash, payment_secret, payment_id)
524524
}
525525

526526
fn send_spontaneous_payment(
527-
&self, route: &Route, payment_preimage: PaymentPreimage,
528-
) -> Result<PaymentId, PaymentSendFailure> {
529-
let payment_id = PaymentId(sha256::Hash::hash(&payment_preimage.0).into_inner());
530-
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id)
531-
.map(|_| payment_id)
527+
&self, route: &Route, payment_preimage: PaymentPreimage, payment_id: PaymentId,
528+
) -> Result<(), PaymentSendFailure> {
529+
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id).map(|_| ())
532530
}
533531

534532
fn retry_payment(

0 commit comments

Comments
 (0)