Skip to content

Conversation

@tnull
Copy link
Collaborator

@tnull tnull commented Jun 14, 2024

Fixes #280.
Fixes #57.

Previously, we wouldn't allow invoice generation for third-party payment hashes. However, as some users require this feature, we here allow them to generate invoices for which they supply the payment hashes, allowing them to retrieve the preimage out-of-bound.

To this end, we add receive_for_hash and receive_variable_amount_for_hash variants to the Bolt11Payment handler. Then, when we receive the payment, we emit a new PaymentClaimable event that users need to call claim_manually for with the correct preimage. Note that this currently only implements the BOLT11 flow as for BOLT12 additions to LDK will be necessary that are pending the next release.

We also add a commit resolving #57, i.e., adding a latest_update_timestamp field to PaymentDetails which allows to filter and sort payment based on how recent they are.

@tnull tnull requested a review from jkczyz June 14, 2024 13:19
// If this is known by the store but ChannelManager doesn't know the preimage,
// the payment has been registered via `_for_hash` variants and needs to be manually claimed via
// user interaction.
match info.kind {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I'm currently not fully sure how we'll be able to cover the BOLT12 flow, I refrained from adding a new PaymentKind for these kind of payments here. Instead, I'm just leaning on us knowing the payment but ChannelManager not knowing the preimage. We might however want to 'upgrade' to a more expressive PaymentKind once we include the BOLT12 flow. Not sure if you have an opinion on this, @jkczyz?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to differentiate them? Checking PaymentPurpose::preimage().is_none() irrespective of the variant should work, IIUC.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to differentiate them? Checking PaymentPurpose::preimage().is_none() irrespective of the variant should work, IIUC.

Well, for BOLT12 the flow will be a bit different, as we currently don't register inbound payments with the payment store. We'll then need to mark them as pending in the new event I guess. At that point we could consider whether we want to add a different PaymentKind variant, or, as we'll generally aim to move the invoice/offer registration to an to-be-added invoice/offer storage could consider whether to add a is_hold or similar flag there. At least these are things we could discuss in the future, in any case for that reason I'd kept it as simple as possible for now.

@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch from 6f363ca to 32bf91c Compare June 14, 2024 13:34
@tnull tnull added this to the 0.3 milestone Jun 14, 2024
/// payment hash.
///
/// [`claim_manually`]: Self::claim_manually
pub fn receive_for_hash(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a high-level question, but I wonder if we should use either:

  • a builder for the parameters so that there are fewer methods or
  • change the calling structure such that you would pick send/receive prior to bolt11/bolt12/onchain, while also using some builder to add the amount/hash/etc.

For the latter, the call might looks something like:

node.inbound_payment()
    .bolt12()
    .amount(10_000)
    .payment_hash(PaymentHash([42; 32])
    .receive();

I can't recall our earlier discussions around this. I seem to recall maybe some concern around bindings, though.

Copy link
Collaborator Author

@tnull tnull Jun 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so generally I'd prefer to try to keep the API as simple method calls at this point as it's easier to pick-up/experiment with rather than to having to understand a builder pattern (especially in bindings languages where it might not be as common/entirely uncommon).

Also, exposing builder in Uniffi is bit awkward as we can't take &mut self or mut self there, which would require us to always wrap an inner builder (see NodeBuilder). However, we could of course work around that via some smart macros that would allow to do that automatically for us, if we find we should do this for many more places.

Regarding the call structure: I'm not entirely convinced adding one more layer to the API much improves simplicity at this point. If we find that we need to keep adding methods, we should probably revisit this idea, however, I really hope that we should be close to done covering additional use-cases by now. Btw, we're about to get two additional payment handlers (payjoin and unified_qr) that will partially re-use/combine the bolt11/bolt12/onchain handlers.

TLDR: Not sure we should go further at this point in time, but agree that we might want to reevaluate the API structure in the future, especially when we're positive that we provide a simplified version. For that I'd still like to learn a bit more about changes-to-come (e.g. regarding human-readable names) and user requirements though. Seems like a something we should reevaluate for ~v0.5 or so?

// If this is known by the store but ChannelManager doesn't know the preimage,
// the payment has been registered via `_for_hash` variants and needs to be manually claimed via
// user interaction.
match info.kind {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to differentiate them? Checking PaymentPurpose::preimage().is_none() irrespective of the variant should work, IIUC.

@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch 3 times, most recently from 1c5dbd5 to ddd34d9 Compare June 17, 2024 09:02
@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch 2 times, most recently from 7fb9eef to 36838f7 Compare June 17, 2024 13:47
@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch from 36838f7 to c841e7f Compare June 17, 2024 18:35
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go ahead and squash. I'll take a final look in the morning.

@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch from c841e7f to 32fb2a8 Compare June 18, 2024 07:38
@tnull
Copy link
Collaborator Author

tnull commented Jun 18, 2024

Go ahead and squash. I'll take a final look in the morning.

Squashed without additional changes.

Comment on lines +293 to +307
if let Some(expected_amount_msat) = details.amount_msat {
if claimable_amount_msat < expected_amount_msat {
log_error!(
self.logger,
"Failed to manually claim payment {} as the claimable amount is less than expected",
payment_id
);
return Err(Error::InvalidAmount);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we don't do this for the user prior to generating an event? If not, they wouldn't need to pass the amount here.

Copy link
Collaborator Author

@tnull tnull Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that but went this way as it felt like breaking the API contract if we just auto-fail in this case. At least with the current version users decide a) exactly when they'd like to fail and b) could bend the value if they somehow decided it's fine for them to claim less than expected. Let me know if you think immediately failing if the amounts don't match would be much preferable though..

tnull added 5 commits June 19, 2024 11:25
... requiring users to manucally claim them.
.. we previously got reports from users trying to pay their own JIT
invoice, which we currently don't support (and possibly never will).

In order to avoid entering any weird states, we just reject our own
circular payment.
.. which allows to filter and sort payment based on how recent they are.
@tnull tnull force-pushed the 2024-06-manual-payment-hash-registrations branch from 32fb2a8 to a678c5e Compare June 19, 2024 09:32
@tnull
Copy link
Collaborator Author

tnull commented Jun 19, 2024

Force-pushed with the following changes:

> git diff-tree -U2 32fb2a8 a678c5e
diff --git a/src/event.rs b/src/event.rs
index a0363ef..838df42 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -87,7 +87,7 @@ pub enum Event {
        /// A payment for a previously-registered payment hash has been received.
        ///
-       /// This needs to be manually claimed by supplying the correct pre-image to [`claim_for_hash`].
+       /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`].
        ///
-       /// If the the provided parameters don't match the expectations or the pre-image can't be
+       /// If the the provided parameters don't match the expectations or the preimage can't be
        /// retrieved in time, should be failed-back via [`fail_for_hash`].
        ///
diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs
index a543101..e8d030b 100644
--- a/src/payment/bolt11.rs
+++ b/src/payment/bolt11.rs
@@ -266,5 +266,5 @@ impl Bolt11Payment {
        /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`].
        ///
-       /// This should be called in reponse to a [`PaymentClaimable`] event as soon as the pre-image is
+       /// This should be called in reponse to a [`PaymentClaimable`] event as soon as the preimage is
        /// available.
        ///
@@ -323,5 +323,5 @@ impl Bolt11Payment {
        ///
        /// This should be called in reponse to a [`PaymentClaimable`] event if the payment needs to be
-       /// failed back, e.g., if the correct pre-image can't be retrieved in time before the claim
+       /// failed back, e.g., if the correct preimage can't be retrieved in time before the claim
        /// deadline has been reached.
        ///
@@ -473,5 +473,5 @@ impl Bolt11Payment {
                                .get_payment_preimage(payment_hash, payment_secret.clone())
                                .ok();
-                       debug_assert!(res.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the pre-image by now.");
+                       debug_assert!(res.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the preimage by now.");
                        res
                } else {
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index f587b0c..5959bd5 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -609,5 +609,5 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
        assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. }));

-       // Test manually registered/claimed payments.
+       // Test claiming manually registered payments.
        let invoice_amount_3_msat = 5_532_000;
        let manual_preimage = PaymentPreimage([42u8; 32]);
@@ -646,4 +646,50 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
        assert!(matches!(node_b.payment(&manual_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. }));

+       // Test failing manually registered payments.
+       let invoice_amount_4_msat = 5_532_000;
+       let manual_fail_preimage = PaymentPreimage([43u8; 32]);
+       let manual_fail_payment_hash =
+               PaymentHash(Sha256::hash(&manual_fail_preimage.0).to_byte_array());
+       let manual_fail_invoice = node_b
+               .bolt11_payment()
+               .receive_for_hash(invoice_amount_3_msat, &"asdf", 9217, manual_fail_payment_hash)
+               .unwrap();
+       let manual_fail_payment_id = node_a.bolt11_payment().send(&manual_fail_invoice).unwrap();
+
+       expect_payment_claimable_event!(
+               node_b,
+               manual_fail_payment_id,
+               manual_fail_payment_hash,
+               invoice_amount_4_msat
+       );
+       node_b.bolt11_payment().fail_for_hash(manual_fail_payment_hash).unwrap();
+       expect_event!(node_a, PaymentFailed);
+       assert_eq!(node_a.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed);
+       assert_eq!(
+               node_a.payment(&manual_fail_payment_id).unwrap().direction,
+               PaymentDirection::Outbound
+       );
+       assert_eq!(
+               node_a.payment(&manual_fail_payment_id).unwrap().amount_msat,
+               Some(invoice_amount_4_msat)
+       );
+       assert!(matches!(
+               node_a.payment(&manual_fail_payment_id).unwrap().kind,
+               PaymentKind::Bolt11 { .. }
+       ));
+       assert_eq!(node_b.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed);
+       assert_eq!(
+               node_b.payment(&manual_fail_payment_id).unwrap().direction,
+               PaymentDirection::Inbound
+       );
+       assert_eq!(
+               node_b.payment(&manual_fail_payment_id).unwrap().amount_msat,
+               Some(invoice_amount_4_msat)
+       );
+       assert!(matches!(
+               node_b.payment(&manual_fail_payment_id).unwrap().kind,
+               PaymentKind::Bolt11 { .. }
+       ));
+
        // Test spontaneous/keysend payments
        println!("\nA send_spontaneous_payment");
@@ -677,6 +723,6 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
                PaymentKind::Spontaneous { .. }
        ));
-       assert_eq!(node_a.list_payments().len(), 5);
-       assert_eq!(node_b.list_payments().len(), 6);
+       assert_eq!(node_a.list_payments().len(), 6);
+       assert_eq!(node_b.list_payments().len(), 7);

        println!("\nB close_channel (force: {})", force_close);

/// [`PaymentClaimable`]: crate::Event::PaymentClaimable
/// [`PaymentReceived`]: crate::Event::PaymentReceived
pub fn claim_for_hash(
&self, payment_hash: PaymentHash, claimable_amount_msat: u64, preimage: PaymentPreimage,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove the payment_hash field and calculate it from the preimage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but I intentionally let the users supply it as the API is focused around custom payment hashes. They will however also know it from PaymentClaimable.

@tnull tnull merged commit b180a65 into lightningdevkit:main Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to manually register and claim payments Consider adding last_updated field to PaymentDetails

3 participants