Skip to content

Commit b434d50

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 0e879c0 commit b434d50

File tree

3 files changed

+127
-49
lines changed

3 files changed

+127
-49
lines changed

lightning-invoice/src/payment.rs

Lines changed: 116 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,76 @@ 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
365+
/// as the payment is still pending. Once the payment completes or fails, you must ensure that
366+
/// a second payment with the same [`PaymentHash`] is never sent.
367+
///
368+
/// If you wish to use a different payment idempotency token, see
369+
/// [`Self::pay_invoice_with_id`].
352370
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {
371+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
372+
self.pay_invoice_with_id(invoice, payment_id).map(|()| payment_id)
373+
}
374+
375+
/// Pays the given [`Invoice`] with a custom idempotency key, caching the invoice for later use
376+
/// in case a retry is needed.
377+
///
378+
/// Note that idempotency is only guaranteed as long as the payment is still pending. Once the
379+
/// payment completes or fails, no idempotency guarantees are made.
380+
///
381+
/// You should ensure that the [`Invoice::payment_hash`] is unique and the same [`PaymentHash`]
382+
/// has never been paid before.
383+
///
384+
/// See [`Self::pay_invoice`] for a variant which uses the [`PaymentHash`] for the idempotency
385+
/// token.
386+
pub fn pay_invoice_with_id(&self, invoice: &Invoice, payment_id: PaymentId) -> Result<(), PaymentError> {
353387
if invoice.amount_milli_satoshis().is_none() {
354388
Err(PaymentError::Invoice("amount missing"))
355389
} else {
356-
self.pay_invoice_using_amount(invoice, None)
390+
self.pay_invoice_using_amount(invoice, None, payment_id)
357391
}
358392
}
359393

360394
/// Pays the given zero-value [`Invoice`] using the given amount, caching it for later use in
361395
/// case a retry is needed.
362396
///
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.
397+
/// [`Invoice::payment_hash`] is used as the [`PaymentId`], which ensures idempotency as long
398+
/// as the payment is still pending. Once the payment completes or fails, you must ensure that
399+
/// a second payment with the same [`PaymentHash`] is never sent.
400+
///
401+
/// If you wish to use a different payment idempotency token, see
402+
/// [`Self::pay_zero_value_invoice_with_id`].
366403
pub fn pay_zero_value_invoice(
367404
&self, invoice: &Invoice, amount_msats: u64
368405
) -> Result<PaymentId, PaymentError> {
406+
let payment_id = PaymentId(invoice.payment_hash().into_inner());
407+
self.pay_zero_value_invoice_with_id(invoice, amount_msats, payment_id).map(|()| payment_id)
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, no idempotency guarantees are made.
415+
///
416+
/// You should ensure that the [`Invoice::payment_hash`] is unique and the same [`PaymentHash`]
417+
/// has never been paid before.
418+
///
419+
/// See [`Self::pay_zero_value_invoice`] for a variant which uses the [`PaymentHash`] for the
420+
/// idempotency token.
421+
pub fn pay_zero_value_invoice_with_id(
422+
&self, invoice: &Invoice, amount_msats: u64, payment_id: PaymentId
423+
) -> Result<(), PaymentError> {
369424
if invoice.amount_milli_satoshis().is_some() {
370425
Err(PaymentError::Invoice("amount unexpected"))
371426
} else {
372-
self.pay_invoice_using_amount(invoice, Some(amount_msats))
427+
self.pay_invoice_using_amount(invoice, Some(amount_msats), payment_id)
373428
}
374429
}
375430

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

381436
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
@@ -398,7 +453,7 @@ where
398453
};
399454

400455
let send_payment = |route: &Route| {
401-
self.payer.send_payment(route, payment_hash, &payment_secret)
456+
self.payer.send_payment(route, payment_hash, &payment_secret, payment_id)
402457
};
403458

404459
self.pay_internal(&route_params, payment_hash, send_payment)
@@ -408,13 +463,39 @@ where
408463
/// Pays `pubkey` an amount using the hash of the given preimage, caching it for later use in
409464
/// case a retry is needed.
410465
///
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.
466+
/// The hash of the [`PaymentPreimage`] is used as the [`PaymentId`], which ensures idempotency
467+
/// as long as the payment is still pending. Once the payment completes or fails, you must
468+
/// ensure that a second payment with the same [`PaymentPreimage`] is never sent.
413469
pub fn pay_pubkey(
414470
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, amount_msats: u64,
415471
final_cltv_expiry_delta: u32
416472
) -> Result<PaymentId, PaymentError> {
417473
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
474+
let payment_id = PaymentId(payment_hash.0);
475+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
476+
final_cltv_expiry_delta)
477+
.map(|()| payment_id)
478+
}
479+
480+
/// Pays `pubkey` an amount using the hash of the given preimage, caching it for later use in
481+
/// case a retry is needed.
482+
///
483+
/// The hash of the [`PaymentPreimage`] is used as the [`PaymentId`], which ensures idempotency
484+
/// as long as the payment is still pending. Once the payment completes or fails, you must
485+
/// ensure that a second payment with the same [`PaymentPreimage`] is never sent.
486+
pub fn pay_pubkey_with_id(
487+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_id: PaymentId,
488+
amount_msats: u64, final_cltv_expiry_delta: u32
489+
) -> Result<(), PaymentError> {
490+
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner());
491+
self.do_pay_pubkey(pubkey, payment_preimage, payment_hash, payment_id, amount_msats,
492+
final_cltv_expiry_delta)
493+
}
494+
495+
fn do_pay_pubkey(
496+
&self, pubkey: PublicKey, payment_preimage: PaymentPreimage, payment_hash: PaymentHash,
497+
payment_id: PaymentId, amount_msats: u64, final_cltv_expiry_delta: u32
498+
) -> Result<(), PaymentError> {
418499
match self.payment_cache.lock().unwrap().entry(payment_hash) {
419500
hash_map::Entry::Occupied(_) => return Err(PaymentError::Invoice("payment pending")),
420501
hash_map::Entry::Vacant(entry) => entry.insert(PaymentInfo::new()),
@@ -427,15 +508,15 @@ where
427508
};
428509

429510
let send_payment = |route: &Route| {
430-
self.payer.send_spontaneous_payment(route, payment_preimage)
511+
self.payer.send_spontaneous_payment(route, payment_preimage, payment_id)
431512
};
432513
self.pay_internal(&route_params, payment_hash, send_payment)
433514
.map_err(|e| { self.payment_cache.lock().unwrap().remove(&payment_hash); e })
434515
}
435516

436-
fn pay_internal<F: FnOnce(&Route) -> Result<PaymentId, PaymentSendFailure> + Copy>(
517+
fn pay_internal<F: FnOnce(&Route) -> Result<(), PaymentSendFailure> + Copy>(
437518
&self, params: &RouteParameters, payment_hash: PaymentHash, send_payment: F,
438-
) -> Result<PaymentId, PaymentError> {
519+
) -> Result<(), PaymentError> {
439520
#[cfg(feature = "std")] {
440521
if has_expired(params) {
441522
log_trace!(self.logger, "Invoice expired prior to send for payment {}", log_bytes!(payment_hash.0));
@@ -452,11 +533,11 @@ where
452533
).map_err(|e| PaymentError::Routing(e))?;
453534

454535
match send_payment(&route) {
455-
Ok(payment_id) => {
536+
Ok(()) => {
456537
for path in route.paths {
457538
self.process_path_inflight_htlcs(payment_hash, path);
458539
}
459-
Ok(payment_id)
540+
Ok(())
460541
},
461542
Err(e) => match e {
462543
PaymentSendFailure::ParameterError(_) => Err(e),
@@ -491,13 +572,13 @@ where
491572
// consider the payment sent, so return `Ok()` here, ignoring any retry
492573
// errors.
493574
let _ = self.retry_payment(payment_id, payment_hash, &retry_data);
494-
Ok(payment_id)
575+
Ok(())
495576
} else {
496577
// This may happen if we send a payment and some paths fail, but
497578
// only due to a temporary monitor failure or the like, implying
498579
// they're really in-flight, but we haven't sent the initial
499580
// HTLC-Add messages yet.
500-
Ok(payment_id)
581+
Ok(())
501582
}
502583
},
503584
},
@@ -2056,13 +2137,13 @@ mod tests {
20562137
self
20572138
}
20582139

2059-
fn check_attempts(&self) -> Result<PaymentId, PaymentSendFailure> {
2140+
fn check_attempts(&self) -> Result<(), PaymentSendFailure> {
20602141
let mut attempts = self.attempts.borrow_mut();
20612142
*attempts += 1;
20622143

20632144
match self.failing_on_attempt.borrow_mut().remove(&*attempts) {
20642145
Some(failure) => Err(failure),
2065-
None => Ok(PaymentId([1; 32])),
2146+
None => Ok(())
20662147
}
20672148
}
20682149

@@ -2100,15 +2181,15 @@ mod tests {
21002181

21012182
fn send_payment(
21022183
&self, route: &Route, _payment_hash: PaymentHash,
2103-
_payment_secret: &Option<PaymentSecret>
2104-
) -> Result<PaymentId, PaymentSendFailure> {
2184+
_payment_secret: &Option<PaymentSecret>, _payment_id: PaymentId,
2185+
) -> Result<(), PaymentSendFailure> {
21052186
self.check_value_msats(Amount::ForInvoice(route.get_total_amount()));
21062187
self.check_attempts()
21072188
}
21082189

21092190
fn send_spontaneous_payment(
2110-
&self, route: &Route, _payment_preimage: PaymentPreimage,
2111-
) -> Result<PaymentId, PaymentSendFailure> {
2191+
&self, route: &Route, _payment_preimage: PaymentPreimage, _payment_id: PaymentId,
2192+
) -> Result<(), PaymentSendFailure> {
21122193
self.check_value_msats(Amount::Spontaneous(route.get_total_amount()));
21132194
self.check_attempts()
21142195
}
@@ -2117,7 +2198,7 @@ mod tests {
21172198
&self, route: &Route, _payment_id: PaymentId
21182199
) -> Result<(), PaymentSendFailure> {
21192200
self.check_value_msats(Amount::OnRetry(route.get_total_amount()));
2120-
self.check_attempts().map(|_| ())
2201+
self.check_attempts()
21212202
}
21222203

21232204
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
@@ -536,18 +536,16 @@ where
536536
}
537537

538538
fn send_payment(
539-
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>
540-
) -> Result<PaymentId, PaymentSendFailure> {
541-
let payment_id = PaymentId(payment_hash.0);
542-
self.send_payment(route, payment_hash, payment_secret, payment_id).map(|()| payment_id)
539+
&self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
540+
payment_id: PaymentId
541+
) -> Result<(), PaymentSendFailure> {
542+
self.send_payment(route, payment_hash, payment_secret, payment_id)
543543
}
544544

545545
fn send_spontaneous_payment(
546-
&self, route: &Route, payment_preimage: PaymentPreimage,
547-
) -> Result<PaymentId, PaymentSendFailure> {
548-
let payment_id = PaymentId(sha256::Hash::hash(&payment_preimage.0).into_inner());
549-
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id)
550-
.map(|_| payment_id)
546+
&self, route: &Route, payment_preimage: PaymentPreimage, payment_id: PaymentId,
547+
) -> Result<(), PaymentSendFailure> {
548+
self.send_spontaneous_payment(route, Some(payment_preimage), payment_id).map(|_| ())
551549
}
552550

553551
fn retry_payment(

lightning/src/ln/channelmanager.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3614,11 +3614,10 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
36143614
fn remove_stale_resolved_payments(&self) {
36153615
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
36163616
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
3617-
// this could race the user making a duplicate payment call (and relying on our idempotency
3618-
// guarantees on the PaymentId to avoid actually sending). Instead, we wait a few timer
3619-
// ticks to do the actual removal. This should be more than sufficient to ensure any
3620-
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
3621-
// processed complete.
3617+
// this could race the user making a duplicate send_payment call and our idempotency
3618+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
3619+
// removal. This should be more than sufficient to ensure any `send_payment` calls that
3620+
// were made at the same time the `PaymentSent` event was being processed complete.
36223621
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
36233622
let pending_events = self.pending_events.lock().unwrap();
36243623
pending_outbound_payments.retain(|payment_id, payment| {

0 commit comments

Comments
 (0)