-
Notifications
You must be signed in to change notification settings - Fork 427
Allow setting an HRN in invoice_requests built by pay_for_offer
#3903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
👋 Thanks for assigning @jkczyz as a reviewer! |
befb3f2 to
076b2b3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I see the motivation for these changes, I'm not super convinced about some of the API changes in this PR.
lightning/src/ln/channelmanager.rs
Outdated
| /// | ||
| #[cfg_attr( | ||
| feature = "dnssec", | ||
| doc = "Note that setting this will cause [`ChannelManager::pay_for_offer_from_human_readable_name`] to fail." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is confusing. Why move everything to this struct if we introduce a footgun for the user?
More generally, do we really care that much that the LDK method has three more arguments or so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its not a huge deal, no, but it did seem like an opportunity to hide parameters we expect to never actually be used behind a default(). Especially the quantity just feels like a weird thing to put in the top-level function signature, though also the routing parameters.
I'm open to dropping the move here (or, honestly, dropping the quantity argument entirely and just setting it to 1 if the offer needs it), if you feel strongly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(or, honestly, dropping the quantity argument entirely and just setting it to 1 if the offer needs it), if you feel strongly.
Hmm, not sure if we can just drop it, but it does have some footguns that would be nice to tackle at some point, see for example #3233
But presumably @jkczyz would have a stronger/better informed view on how to fix this/how the API should look like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems pay_for_offer_from_human_readable_name should take a different type and have it translate it to OptionalOfferPaymentInfo. Or alternatively just leave quantity as a parameter to pay_for_offer.
Or we could introduce an OfferSupportingQuantity wrapper type on Offer -- which can only be created when Offer::expects_quantity is true -- and have a separate pay_for_offer_using_quantity method taking that type and having a required quantity parameter. Then drop the quantity parameter from pay_for_offer -- setting it internally to 1 if Offer::expects_quantity is true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we could introduce an OfferSupportingQuantity wrapper type on Offer -- which can only be created when Offer::expects_quantity is true -- and have a separate pay_for_offer_using_quantity method taking that type and having a required quantity parameter. Then drop the quantity parameter from pay_for_offer -- setting it internally to 1 if Offer::expects_quantity is true.
Not sure why we need a separate wrapper type? I went ahead and pushed a fixup that adds a separate pay_for_offer_with_quantity type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that's fine by me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapper would just remove a possible error where otherwise an offer not expecting a quantity is passed to pay_for_offer_with_quantity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would just move the error somewhere else, though? Instead of getting an error back from pay_for_offer_with_quantity you get a Result when you convert to the wrapper, which is just more code for the user.
lightning/src/ln/channelmanager.rs
Outdated
| pub fn pay_for_offer( | ||
| &self, offer: &Offer, amount_msats: Option<u64>, payment_id: PaymentId, | ||
| optional_info: OptionalOfferPaymentInfo, | ||
| optional_info: OptionalOfferPaymentInfo, derived_from_hrn: Option<HumanReadableName>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do that, why not actually lean into the switch to bitcoin-payment-instructions and drop the pay_for_offer_from_hrn method. I don't think any of our users really implements HRNs yet, so we wouldn't even break API for anybody, AFAIK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, we could. Its still useful for a lightning-only wallet, but maybe we don't care too much about those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What determines if something goes in OptionalOfferPaymentInfo vs added as a new parameter. Seems derived_from_hrn would rarely be Some, so why add another parameter for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking was basically "anything that we expect users to need to think about" ie either "we dont have a good default and the user knows best" or "if the user mis-configures this, that's really bad". derived_from_hrn isn't expected to be set that much, but it falls into the second category.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I find the parameter a bit odd, especially now that we try to split out most to a params object.
Yea, we could. Its still useful for a lightning-only wallet, but maybe we don't care too much about those?
Do we believe there ever will be a truly Lightning-only wallet that doesn't support to pay any onchain (or other L2) addresses?
IMO, even if we think this will ever be a thing, we should probably not optimize our API around that. Now that we added pay_for_offer_with_quantity, could we add a similar pay_for_offer_derived_from_hrn method that allows setting the HumanReadableName param, and just drop the original pay_for_offer_from_human_readable_name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I find the parameter a bit odd, especially now that we try to split out most to a params object.
Sure, but in this case it falls into the "a default becomes dangerous if it actually needed to be configured" case, so its kinda hard to move...
Do we believe there ever will be a truly Lightning-only wallet that doesn't support to pay any onchain (or other L2) addresses?
Maybe not anymore, I dunno. Phoenix was for a long time pre-splicing, I could see an eventual future where you don't really need L1 payments, and all L2s just "accept lightning" so it doesn't matter. I'm okay with removing the pay_for_offer_from_hrn stuff if you feel strongly that we should just rip it out.
IMO, even if we think this will ever be a thing, we should probably not optimize our API around that.
Hmm? This is the opposite case - the new API is important for wallets that do support L1 payments, and thus need to use bitcoin-payment-instructions to do resolutions.
Now that we added pay_for_offer_with_quantity, could we add a similar pay_for_offer_derived_from_hrn method that allows setting the HumanReadableName param, and just drop the original pay_for_offer_from_human_readable_name?
My fear is that a wallet will use bitcoin-payment-instructions to resolve an HRN, then happily pass the offer to pay_for_offer, since that's "the offer payment method". In practice, most users won't ever read through our long docs, so payments to BOLT 12 HRNs will just fail (while LNURL requests will succeed so the wallet author won't notice until later while BOLT 12/353 HRNs aren't broadly deployed yet).
I suppose we could try to address this by having bitcoin-payment-instructions instead have a pay_with_ldk_channelmanager method that takes the PaymentId and OptionalOfferPaymentParams and passes them through to a separate method, but it still leaves the API quite easy to misuse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we have bitcoin-payment-instructions return an OfferFromHRN (wrapping the offer and name) that we could pass to our own pay_for_offer_derived_from_hrn ? Then we don't need to pass None everywhere pay_for_offer is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it does still seem easy to accidentally call channelmanager.pay_for_offer(offer_from_hrn.offer()...), though certainly less so than exposing the offer directly. I'm a bit torn on it but willing to go that direction if y'all prefer.
lightning/src/ln/channelmanager.rs
Outdated
| pub fn pay_for_offer( | ||
| &self, offer: &Offer, amount_msats: Option<u64>, payment_id: PaymentId, | ||
| optional_info: OptionalOfferPaymentInfo, | ||
| optional_info: OptionalOfferPaymentInfo, derived_from_hrn: Option<HumanReadableName>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What determines if something goes in OptionalOfferPaymentInfo vs added as a new parameter. Seems derived_from_hrn would rarely be Some, so why add another parameter for it?
lightning/src/ln/channelmanager.rs
Outdated
| /// | ||
| #[cfg_attr( | ||
| feature = "dnssec", | ||
| doc = "Note that setting this will cause [`ChannelManager::pay_for_offer_from_human_readable_name`] to fail." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems pay_for_offer_from_human_readable_name should take a different type and have it translate it to OptionalOfferPaymentInfo. Or alternatively just leave quantity as a parameter to pay_for_offer.
Or we could introduce an OfferSupportingQuantity wrapper type on Offer -- which can only be created when Offer::expects_quantity is true -- and have a separate pay_for_offer_using_quantity method taking that type and having a required quantity parameter. Then drop the quantity parameter from pay_for_offer -- setting it internally to 1 if Offer::expects_quantity is true.
7a7a5d1 to
a2daa01
Compare
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixups look good to me. Some more comments on the API design
lightning/src/ln/channelmanager.rs
Outdated
| /// | ||
| #[cfg_attr( | ||
| feature = "dnssec", | ||
| doc = "Note that setting this will cause [`ChannelManager::pay_for_offer_from_human_readable_name`] to fail." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that's fine by me.
lightning/src/ln/channelmanager.rs
Outdated
| pub fn pay_for_offer( | ||
| &self, offer: &Offer, amount_msats: Option<u64>, payment_id: PaymentId, | ||
| optional_info: OptionalOfferPaymentInfo, | ||
| optional_info: OptionalOfferPaymentInfo, derived_from_hrn: Option<HumanReadableName>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I find the parameter a bit odd, especially now that we try to split out most to a params object.
Yea, we could. Its still useful for a lightning-only wallet, but maybe we don't care too much about those?
Do we believe there ever will be a truly Lightning-only wallet that doesn't support to pay any onchain (or other L2) addresses?
IMO, even if we think this will ever be a thing, we should probably not optimize our API around that. Now that we added pay_for_offer_with_quantity, could we add a similar pay_for_offer_derived_from_hrn method that allows setting the HumanReadableName param, and just drop the original pay_for_offer_from_human_readable_name?
a2daa01 to
297896c
Compare
|
Okay, sorry for the extended delay here. I went ahead and rebased (and squashed since its been so long) and took the suggestion at #3903 (comment) (which will have |
|
I do think we should include this in LDK 0.2 (and updated the milestone to match) since its (I presume) important for ldk-node to switch to |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3903 +/- ##
==========================================
- Coverage 88.33% 87.81% -0.53%
==========================================
Files 177 176 -1
Lines 131896 131737 -159
Branches 131896 131737 -159
==========================================
- Hits 116512 115681 -831
- Misses 12728 13421 +693
+ Partials 2656 2635 -21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
1 similar comment
1 similar comment
1 similar comment
| payer_note: None, | ||
| route_params_config: Default::default(), | ||
| #[cfg(feature = "std")] | ||
| retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use something longer that is closer to how long Retry::Attempts(3) may take?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retry::Attempts(3) should hopefully take around 2 seconds, I believe.
| let params = RouteParametersConfig::default(); | ||
| nodes[0] | ||
| .node | ||
| .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably the different Retry::Attempts values used throughout these tests weren't necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently not since they pass :) cc @valentinewallace
lightning/src/ln/channelmanager.rs
Outdated
| let create_pending_payment_fn = |invoice_request: &InvoiceRequest, nonce| { | ||
| let expiration = StaleExpiration::TimerTicks(1); | ||
| let retryable_invoice_request = RetryableInvoiceRequest { | ||
| invoice_request: invoice_request.clone(), | ||
| nonce, | ||
| needs_retry: true, | ||
| }; | ||
| self.pending_outbound_payments | ||
| .add_new_awaiting_invoice( | ||
| payment_id, | ||
| expiration, | ||
| optional_params.retry_strategy, | ||
| optional_params.route_params_config, | ||
| Some(retryable_invoice_request), | ||
| ) | ||
| .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little different from my earlier suggestions, but to avoid duplicating this, we could consider:
- implementing
AsRef<Offer>forOfferFromHrn - adding an
ToHumanReadableNametrait implemented by bothOfferandOfferFromHrnwith a method returningOption<HumanReadableName> - using a single method taking
O: AsRef<Offer> + ToHumanReadableName
That way OfferFromHrn could be private to help avoid misuse. Though maybe explicit methods for each case would be more intuitive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- implementing
AsRef<Offer>forOfferFromHrn
Isn't implementing AsRef for anything besides straight wrappers or smart pointers considered a bit of an anti-pattern? Maybe we could just do impl From<OfferFromHrn> for Offer> and have the pay_for_offer take an Into<Offer>?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I very specifically want to avoid it being too easy to convert an OfferFromHrn to an Offer - depending on how its done the compiler may suggest "didn't you mean to into before calling pay_for_offer?" which would be an incorrect suggestion.
297896c to
04cb808
Compare
|
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
Now that these do not allocate, we should allow them to be `Copy`ed.
04cb808 to
71fe9d7
Compare
|
Rebased cause I had to and squashed fixups while I was at it. |
| /// Pays for an [`Offer`] which was built by resolving a human readable name. It is otherwise | ||
| /// identical to [`Self::pay_for_offer`]. | ||
| pub fn pay_for_offer_from_hrn( | ||
| &self, offer: &OfferFromHrn, amount_msats: u64, payment_id: PaymentId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API contract violation: The pay_for_offer_from_hrn method takes amount_msats: u64 (required) while pay_for_offer takes amount_msats: Option<u64> (optional). This inconsistency could cause confusion and runtime errors when users expect similar behavior between the two methods. Both methods should have consistent parameter types for the same logical operation.
| &self, offer: &OfferFromHrn, amount_msats: u64, payment_id: PaymentId, | |
| &self, offer: &OfferFromHrn, amount_msats: Option<u64>, payment_id: PaymentId, |
Spotted by Diamond
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this be a runtime issue? It should be caught at compile-time. And the API difference is deliberate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I take it OfferFromHrn from is expected have an Offer without an amount?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea.
In a coming commit we'll provide users a different `ChannelManager` method for sending to `Offer`s which were fetched by resolving an HRN. Here, we first ad a wrapper type which can be used by HRN resolution methods to nudge users towards calling the correct payment method on `ChannelManager`.
`ChannelManager::pay_for_offer` and `ChannelManager::pay_for_offer_from_human_readable_name` have accumulated a handful of arguments, most of which we expect users to never actually set. Instead, here, we move `quantity` and `payer_note` (which we expect users to never set) as well as `route_params_config` and `retry_strategy` (which we expect users to generally stick with the defaults on) into a new struct, which implements `Default`. This cleans up a good bit of cruft on payment calls.
For some time we've automatically opened a connection to the blinded path introduction point when we need to send a message we generated. However, the "Limitations" section in `ChannelManager::pay_for_offer` lists having a direct connection as required for the payment to succeed, which is not true. Instead, we simply remove the section from both `pay_for_offer` and `pay_for_offer_from_human_readable_name`.
If a user did their own BIP 353 lookup to fetch an offer from a human readable name, we still want them to be able to use `ChannelManager::pay_for_offer`. Because BIP 353 offer payments require that the `invoice_request` include the human readable name, we need to add an argument to set the `invoice_request` HRN to `pay_for_offer`, which we do here.
71fe9d7 to
dbf40d3
Compare
|
Went ahead and removed the extra clone. $ git diff-tree -U1 71fe9d74f0 dbf40d3fe9
diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs
index ff798388b1..7eb719c104 100644
--- a/lightning/src/offers/offer.rs
+++ b/lightning/src/offers/offer.rs
@@ -846,3 +846,3 @@ impl OfferFromHrn {
InvoiceRequestWithDerivedPayerSigningPubkeyBuilder<'a, 'b>,
- Some(self.hrn.clone())
+ Some(self.hrn)
); |
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good to me, going ahead and landing this.
On a more general note I'm still not the biggest fan of having the different payment APIs (BOLT11/BOLT12/spontaneous) diverge further by splitting out RouteParametersConfig in yet another config object just for BOLT12. We'll probably try to keep them more aligned in LDK Node.
Given that it would only be useful for a Lightning-only wallet that never wants to handle any of the other types of HumanReadableNames, I also still think it would make sense to drop pay_for_offer_from_human_readable_names and rely on the bitcoin-payment-instructions API for the resolution part. This is even more so the case since having both pay_for_offer_from_human_readable_names and pay_for_offer_from_hrn APIs side-by-side is pretty confusing IMO.
Mmm, yea, that's worse than I was thinking lol. Maybe we should deprecate the native-resolution one... |
|
ldk-node broken: lightningdevkit/ldk-node#639 |
and a few other misc cleanups of the
pay_for_offerAPI.