Skip to content

Commit b5f1da6

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 0ae45a2 commit b5f1da6

File tree

2 files changed

+124
-44
lines changed

2 files changed

+124
-44
lines changed

lightning-invoice/src/payment.rs

+117-35
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,18 @@ 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+
/// [`ChannelManager`] provides idempotency for duplicate payments with the same [`PaymentId`].
251+
///
252+
/// In order to trivially ensure idempotency for payments, the default `Payer` implementation
253+
/// reuses the [`PaymentHash`] bytes as the [`PaymentId`]. Custom implementations wishing to
254+
/// provide payment idempotency with a different idempotency key (i.e. [`PaymentId`]) should map
255+
/// the [`Invoice`] or spontaneous payment target pubkey to their own idempotency key.
256+
///
257+
/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager
245258
pub trait Payer {
246259
/// Returns the payer's node id.
247260
fn node_id(&self) -> PublicKey;
@@ -251,13 +264,14 @@ pub trait Payer {
251264

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

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

262276
/// Retries a failed payment path for the [`PaymentId`] using the given [`Route`].
263277
fn retry_payment(&self, route: &Route, payment_id: PaymentId) -> Result<(), PaymentSendFailure>;
@@ -346,36 +360,76 @@ where
346360

347361
/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
348362
///
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.
363+
/// [`Invoice::payment_hash`] is used as the [`PaymentId`], which ensures idempotency as long
364+
/// as the payment is still pending. Once the payment completes or fails, you must ensure that
365+
/// a second payment with the same [`PaymentHash`] is never sent.
366+
///
367+
/// If you wish to use a different payment idempotency token, see
368+
/// [`Self::pay_invoice_with_id`].
352369
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {
370+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
371+
self.pay_invoice_with_id(invoice, payment_id).map(|()| payment_id)
372+
}
373+
374+
/// Pays the given [`Invoice`] with a custom idempotency key, caching the invoice for later use
375+
/// in case a retry is needed.
376+
///
377+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
378+
/// payment completes or fails, no idempotency guarantees are made.
379+
///
380+
/// You should ensure that the [`Invoice::payment_hash`] is unique and the same [`PaymentHash`]
381+
/// has never been paid before.
382+
///
383+
/// See [`Self::pay_invoice`] for a variant which uses the [`PaymentHash`] for the idempotency
384+
/// token.
385+
pub fn pay_invoice_with_id(&self, invoice: &Invoice, payment_id: PaymentId) -> Result<(), PaymentError> {
353386
if invoice.amount_milli_satoshis().is_none() {
354387
Err(PaymentError::Invoice("amount missing"))
355388
} else {
356-
self.pay_invoice_using_amount(invoice, None)
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
397+
/// as the payment is still pending. Once the payment completes or fails, you must ensure that
398+
/// a second payment with the same [`PaymentHash`] is never sent.
399+
///
400+
/// If you wish to use a different payment idempotency token, see
401+
/// [`Self::pay_zero_value_invoice_with_id`].
366402
pub fn pay_zero_value_invoice(
367403
&self, invoice: &Invoice, amount_msats: u64
368404
) -> Result<PaymentId, PaymentError> {
405+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
406+
self.pay_zero_value_invoice_with_id(invoice, amount_msats, payment_id).map(|()| payment_id)
407+
}
408+
409+
/// Pays the given zero-value [`Invoice`] using the given amount and custom idempotency key,
410+
/// caching the invoice for later use in case a retry is needed.
411+
///
412+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
413+
/// payment completes or fails, no idempotency guarantees are made.
414+
///
415+
/// You should ensure that the [`Invoice::payment_hash`] is unique and the same [`PaymentHash`]
416+
/// has never been paid before.
417+
///
418+
/// See [`Self::pay_zero_value_invoice`] for a variant which uses the [`PaymentHash`] for the
419+
/// idempotency token.
420+
pub fn pay_zero_value_invoice_with_id(
421+
&self, invoice: &Invoice, amount_msats: u64, payment_id: PaymentId
422+
) -> Result<(), PaymentError> {
369423
if invoice.amount_milli_satoshis().is_some() {
370424
Err(PaymentError::Invoice("amount unexpected"))
371425
} else {
372-
self.pay_invoice_using_amount(invoice, Some(amount_msats))
426+
self.pay_invoice_using_amount(invoice, Some(amount_msats), payment_id)
373427
}
374428
}
375429

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

381435
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
@@ -398,7 +452,7 @@ where
398452
};
399453

400454
let send_payment = |route: &Route| {
401-
self.payer.send_payment(route, payment_hash, &payment_secret)
455+
self.payer.send_payment(route, payment_hash, &payment_secret, payment_id)
402456
};
403457

404458
self.pay_internal(&route_params, payment_hash, send_payment)
@@ -408,13 +462,41 @@ where
408462
/// Pays `pubkey` an amount using the hash of the given preimage, caching it for later use in
409463
/// case a retry is needed.
410464
///
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.
465+
/// The hash of the [`PaymentPreimage`] is used as the [`PaymentId`], which ensures idempotency
466+
/// as long as the payment is still pending. Once the payment completes or fails, you must
467+
/// ensure that a second payment with the same [`PaymentPreimage`] is never sent.
413468
pub fn pay_pubkey(
414469
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, amount_msats: u64,
415470
final_cltv_expiry_delta: u32
416471
) -> Result<PaymentId, PaymentError> {
417472
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
473+
let payment_id = PaymentId(payment_hash.0);
474+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
475+
final_cltv_expiry_delta)
476+
.map(|()| payment_id)
477+
}
478+
479+
/// Pays `pubkey` an amount using the hash of the given preimage and a custom idempotency key,
480+
/// caching the invoice for later use in case a retry is needed.
481+
///
482+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
483+
/// payment completes or fails, no idempotency guarantees are made.
484+
///
485+
/// You should ensure that the [`PaymentPreimage`] is unique and the corresponding
486+
/// [`PaymentHash`] has never been paid before.
487+
pub fn pay_pubkey_with_id(
488+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_id: PaymentId,
489+
amount_msats: u64, final_cltv_expiry_delta: u32
490+
) -> Result<(), PaymentError> {
491+
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
492+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
493+
final_cltv_expiry_delta)
494+
}
495+
496+
fn do_pay_pubkey(
497+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_hash: PaymentHash,
498+
payment_id: PaymentId, amount_msats: u64, final_cltv_expiry_delta: u32
499+
) -> Result<(), PaymentError> {
418500
match self.payment_cache.lock().unwrap().entry(payment_hash) {
419501
hash_map::Entry::Occupied(_) => return Err(PaymentError::Invoice("payment pending")),
420502
hash_map::Entry::Vacant(entry) => entry.insert(PaymentInfo::new()),
@@ -427,15 +509,15 @@ where
427509
};
428510

429511
let send_payment = |route: &Route| {
430-
self.payer.send_spontaneous_payment(route, payment_preimage)
512+
self.payer.send_spontaneous_payment(route, payment_preimage, payment_id)
431513
};
432514
self.pay_internal(&route_params, payment_hash, send_payment)
433515
.map_err(|e| { self.payment_cache.lock().unwrap().remove(&payment_hash); e })
434516
}
435517

436-
fn pay_internal<F: FnOnce(&Route) -> Result<PaymentId, PaymentSendFailure> + Copy>(
518+
fn pay_internal<F: FnOnce(&Route) -> Result<(), PaymentSendFailure> + Copy>(
437519
&self, params: &RouteParameters, payment_hash: PaymentHash, send_payment: F,
438-
) -> Result<PaymentId, PaymentError> {
520+
) -> Result<(), PaymentError> {
439521
#[cfg(feature = "std")] {
440522
if has_expired(params) {
441523
log_trace!(self.logger, "Invoice expired prior to send for payment {}", log_bytes!(payment_hash.0));
@@ -452,11 +534,11 @@ where
452534
).map_err(|e| PaymentError::Routing(e))?;
453535

454536
match send_payment(&route) {
455-
Ok(payment_id) => {
537+
Ok(()) => {
456538
for path in route.paths {
457539
self.process_path_inflight_htlcs(payment_hash, path);
458540
}
459-
Ok(payment_id)
541+
Ok(())
460542
},
461543
Err(e) => match e {
462544
PaymentSendFailure::ParameterError(_) => Err(e),
@@ -491,13 +573,13 @@ where
491573
// consider the payment sent, so return `Ok()` here, ignoring any retry
492574
// errors.
493575
let _ = self.retry_payment(payment_id, payment_hash, &retry_data);
494-
Ok(payment_id)
576+
Ok(())
495577
} else {
496578
// This may happen if we send a payment and some paths fail, but
497579
// only due to a temporary monitor failure or the like, implying
498580
// they're really in-flight, but we haven't sent the initial
499581
// HTLC-Add messages yet.
500-
Ok(payment_id)
582+
Ok(())
501583
}
502584
},
503585
},
@@ -2056,13 +2138,13 @@ mod tests {
20562138
self
20572139
}
20582140

2059-
fn check_attempts(&self) -> Result<PaymentId, PaymentSendFailure> {
2141+
fn check_attempts(&self) -> Result<(), PaymentSendFailure> {
20602142
let mut attempts = self.attempts.borrow_mut();
20612143
*attempts += 1;
20622144

20632145
match self.failing_on_attempt.borrow_mut().remove(&*attempts) {
20642146
Some(failure) => Err(failure),
2065-
None => Ok(PaymentId([1; 32])),
2147+
None => Ok(())
20662148
}
20672149
}
20682150

@@ -2100,15 +2182,15 @@ mod tests {
21002182

21012183
fn send_payment(
21022184
&self, route: &Route, _payment_hash: PaymentHash,
2103-
_payment_secret: &Option<PaymentSecret>
2104-
) -> Result<PaymentId, PaymentSendFailure> {
2185+
_payment_secret: &Option<PaymentSecret>, _payment_id: PaymentId,
2186+
) -> Result<(), PaymentSendFailure> {
21052187
self.check_value_msats(Amount::ForInvoice(route.get_total_amount()));
21062188
self.check_attempts()
21072189
}
21082190

21092191
fn send_spontaneous_payment(
2110-
&self, route: &Route, _payment_preimage: PaymentPreimage,
2111-
) -> Result<PaymentId, PaymentSendFailure> {
2192+
&self, route: &Route, _payment_preimage: PaymentPreimage, _payment_id: PaymentId,
2193+
) -> Result<(), PaymentSendFailure> {
21122194
self.check_value_msats(Amount::Spontaneous(route.get_total_amount()));
21132195
self.check_attempts()
21142196
}
@@ -2117,7 +2199,7 @@ mod tests {
21172199
&self, route: &Route, _payment_id: PaymentId
21182200
) -> Result<(), PaymentSendFailure> {
21192201
self.check_value_msats(Amount::OnRetry(route.get_total_amount()));
2120-
self.check_attempts().map(|_| ())
2202+
self.check_attempts()
21212203
}
21222204

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

lightning-invoice/src/utils.rs

+7-9
Original file line numberDiff line numberDiff line change
@@ -602,18 +602,16 @@ where
602602
}
603603

604604
fn send_payment(
605-
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>
606-
) -> Result<PaymentId, PaymentSendFailure> {
607-
let payment_id = PaymentId(payment_hash.0);
608-
self.send_payment(route, payment_hash, payment_secret, payment_id).map(|()| payment_id)
605+
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
606+
payment_id: PaymentId
607+
) -> Result<(), PaymentSendFailure> {
608+
self.send_payment(route, payment_hash, payment_secret, payment_id)
609609
}
610610

611611
fn send_spontaneous_payment(
612-
&self, route: &Route, payment_preimage: PaymentPreimage,
613-
) -> Result<PaymentId, PaymentSendFailure> {
614-
let payment_id = PaymentId(sha256::Hash::hash(&payment_preimage.0).into_inner());
615-
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id)
616-
.map(|_| payment_id)
612+
&self, route: &Route, payment_preimage: PaymentPreimage, payment_id: PaymentId,
613+
) -> Result<(), PaymentSendFailure> {
614+
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id).map(|_| ())
617615
}
618616

619617
fn retry_payment(

0 commit comments

Comments
 (0)