-
Notifications
You must be signed in to change notification settings - Fork 370
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
BOLT 12 static invoice encoding and building #3082
BOLT 12 static invoice encoding and building #3082
Conversation
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.
Conceptual feedback appreciated. Left some comments calling out specific things that might warrant discussion :)
if invoice.payment_paths.is_empty() { | ||
return Err(Bolt12SemanticError::MissingPaths); | ||
} | ||
if invoice.offer.chains().len() > 1 { |
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'm currently disallowing offers with multiple chains because they complicate the AP spec + code, given that we’ll need to either add a separate chain
field in the invoice TLV range or include a single invreq TLV in static invoices.
I'm not convinced multi-chain offers will ever make sense for often-offline receivers given they will always be using blinded paths, which don't seem likely to work across chains IIUC. There are currently no LN nodes impls that support running multiple chains simultaneously anyway. I'm sort of in favor of removing multi-chain support from the offers spec.
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 this be checked on the passed Offer
prior to creating the InvoiceContents
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.
True, I went ahead and fixed that in the latest push.
tagged_hash: TaggedHash, | ||
} | ||
|
||
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { |
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.
~5 of these methods could theoretically be DRY'd further with their single-use invoice equivalents, but what makes it tricky is that the equivalent methods' docs reference Refunds
. It seemed not worth spending too much time on given the methods themselves are like 3 lines.
/// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours | ||
/// after `created_at`. | ||
pub fn for_offer_using_keys( | ||
offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration, |
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 considered whether it would make sense to allow creating these invoices from Refunds
. Input welcome here, but my thinking is that if an async payer provides someone a refund then their always-online peer could hold onto a payee's single-use invoice
via OM mailbox until the payer come online. This flow may need some fleshing out down the line, though.
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.
Right... Refund
s are communicated out-of-band, so it seems a StaticInvoice
wouldn't be relevant as it requires user action to read it and thus create the invoice. I'm a little confused about the scenario then because the payee (i.e., the user) should be online at this point though the payer (i.e., creator of the Refund
) could possibly have gone offline before the payee sends them the invoice.
0d9e534
to
dc6dff4
Compare
Pushed a fix for the test. |
Codecov ReportAttention: Patch coverage is
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #3082 +/- ##
==========================================
+ Coverage 89.83% 91.17% +1.33%
==========================================
Files 119 121 +2
Lines 97516 107815 +10299
Branches 97516 107815 +10299
==========================================
+ Hits 87605 98296 +10691
+ Misses 7332 7068 -264
+ Partials 2579 2451 -128 ☔ View full report in Codecov by Sentry. |
dc6dff4
to
5a54218
Compare
Seems fine to me? I don't really have a strong opinion about this vs trying to reuse |
Haven't taken a thorough look yet, but yeah my feeling was to make a separate type for static invoices. Maybe there could be a separate |
A counterpoint might be that with #3078 we might want to have a single invoice type for the event. In that case, maybe we can live with an |
Isn't part of the point of #3078 that people might want to look at the payment hash before they pay, in which case a single type won't help? |
For reference, here are the pros and cons of using a separate type from where I stand: Pros of separate type:
Cons of separate type:
I may be missing some since I didn’t go too far down the same-type road but this is what’s come up so far. |
Feel free to check out I used a separate TLV for static invoices, not 66. |
Is it possible that users may want to set their own keysend preimage? Not sure if |
Respond there regarding the sending case. It's more for receiving when the payment hash is important. |
5a54218
to
8c2700b
Compare
/// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours | ||
/// after `created_at`. | ||
pub fn for_offer_using_keys( | ||
offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration, |
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.
Right... Refund
s are communicated out-of-band, so it seems a StaticInvoice
wouldn't be relevant as it requires user action to read it and thus create the invoice. I'm a little confused about the scenario then because the payee (i.e., the user) should be online at this point though the payer (i.e., creator of the Refund
) could possibly have gone offline before the payee sends them the invoice.
if invoice.payment_paths.is_empty() { | ||
return Err(Bolt12SemanticError::MissingPaths); | ||
} | ||
if invoice.offer.chains().len() > 1 { |
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 this be checked on the passed Offer
prior to creating the InvoiceContents
8c2700b
to
3daa79e
Compare
Added tests and fixed up some things in the latest push so gonna go ahead and take this out of draft. Still a few outstanding TODOs in the PR description. Thanks @jkczyz for the review 🫡 |
3daa79e
to
e81ddb0
Compare
Addressed feedback, this should be good for review. Going to skip adding code example module-level docs until the methods to create these new structs exist on Looks like this needs rebase as well so lmk if I can go ahead with that. |
Yes, please go ahead and rebase. |
Will be useful when we later add support for static BOLT 12 invoices.
Will be useful when we support static BOLT 12 invoices.
e81ddb0
to
74ea2a7
Compare
Will be useful so the docs generated work for static invoices.
Useful for static invoice support.
Will be useful for static invoices.
Minor cleanup to be more concise.
ecf3154
to
fd8b053
Compare
Squashed with no changes. |
/// Paths to the node that may supply the invoice on the recipient's behalf, originating from | ||
/// publicly reachable nodes. Taken from [`Offer::paths`]. | ||
/// | ||
/// [`Offer::paths`]: crate::offers::offer::Offer::paths | ||
pub fn offer_message_paths(&$self) -> &[BlindedPath] { | ||
$contents.offer_message_paths() | ||
} | ||
|
||
/// Paths to the recipient for indicating that a held HTLC is available to claim when they next | ||
/// come online. | ||
pub fn message_paths(&$self) -> &[BlindedPath] { | ||
$contents.message_paths() | ||
} |
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.
It's a bit inconsistent that Bolt12Invoice::message_paths
and StaticInvoice::message_paths
don't correspond to the same thing. We may want to re-think the naming here. Further, message_paths
may not be descriptive enough especially when we also add paths for payment notification to the offer. Though I'm having trouble finding a way to differentiate naming the latter from the one added here.
Possible naming:
request_paths
foroffer_paths
TLVpayment_paths
forinvoice_paths
andinvoice_blindedpay
notification_paths
for paths add tooffer
for upcoming payment notifications- ??? for paths added to
invoice
for async payments
WDYT?
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.
Don't feel super strongly since users are unlikely to be accessing these fields directly, but I'm happy to rename these methods. Since we're pulling in other considerations like payment notifications and renaming offer fields as well, I'm working on a follow-up based on this PR that should be open sometime today, if that works for you. Naming suggestions SGTM.
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.
Opened #3118.
.unwrap() | ||
.build_and_sign(&secp_ctx) | ||
.unwrap(); |
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.
Huh, strange that it's inconsistent with when the first call can fit on one line.
$contents.offer_message_paths() | ||
} | ||
|
||
/// Paths to the recipient for indicating that a held HTLC is available to claim when they next |
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.
For refreshing my understanding, who is holding the HTLC here? Is this for sending held_htlc_available
? Do you have a reference for this flow? I didn't see it in the spec PR other than:
- if
invoice_payment_hash
is unset:
- MUST reject the invoice if
invoice_message_paths
is not present or is empty.- MUST pay asynchronously using the
held_htlc_available
onion message
flow, where the onion message is sent overinvoice_message_paths
.
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.
who is holding the HTLC here?
If the sender is often-offline, then their LSP is holding the HTLC. Otherwise, the sender holds the HTLC themselves until the recipient comes online.
Is this for sending held_htlc_available?
Yep!
For reference, lightning/bolts#1149 is based on lightning/bolts#989, which is the flow referred to in the quote. Let me know if something's unclear.
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.
Ah, thanks! I was only looking at the last commits and not also the first ones. 🤦♂️
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.
Basically LGTM
Will be used in static invoices. Also test that we'll fail to decode if these paths are included in single-use BOLT 12 invoices.
fd8b053
to
391bcbe
Compare
Went ahead and squashed in updates: diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
index c570aed66..6695121b3 100644
--- a/lightning/src/offers/invoice.rs
+++ b/lightning/src/offers/invoice.rs
@@ -135,7 +135,7 @@ use std::time::SystemTime;
pub(crate) const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
-/// Tag for the hash function used when signing a BOLT 12 invoice's merkle root.
+/// Tag for the hash function used when signing a [`Bolt12Invoice`]'s merkle root.
pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
/// Builds a [`Bolt12Invoice`] from either:
@@ -1164,7 +1164,7 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
(176, node_id: PublicKey),
// Only present in `StaticInvoice`s.
- (178, message_paths: (Vec<BlindedPath>, WithoutLength)),
+ (238, message_paths: (Vec<BlindedPath>, WithoutLength)),
});
pub(super) type BlindedPathIter<'a> = core::iter::Map<
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index dd132bde7..77e2b1211 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -17,7 +17,6 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::{
check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter,
BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef,
- SIGNATURE_TAG,
};
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
use crate::offers::merkle::{
@@ -43,8 +42,11 @@ use crate::offers::invoice::is_expired;
#[allow(unused_imports)]
use crate::prelude::*;
-/// Static invoices default to expiring after 24 hours.
-const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24);
+/// Static invoices default to expiring after 2 weeks.
+const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14);
+
+/// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root.
+pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature");
/// A `StaticInvoice` is a reusable payment request corresponding to an [`Offer`].
///
@@ -566,13 +568,13 @@ mod tests {
use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
use crate::ln::inbound_payment::ExpandedKey;
use crate::ln::msgs::DecodeError;
- use crate::offers::invoice::{InvoiceTlvStreamRef, SIGNATURE_TAG};
+ use crate::offers::invoice::InvoiceTlvStreamRef;
use crate::offers::merkle;
use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash};
use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity};
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
use crate::offers::static_invoice::{
- StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
+ StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG,
};
use crate::offers::test_utils::*;
use crate::sign::KeyMaterial; |
$contents.offer_message_paths() | ||
} | ||
|
||
/// Paths to the recipient for indicating that a held HTLC is available to claim when they next |
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.
Ah, thanks! I was only looking at the last commits and not also the first ones. 🤦♂️
Define an interface for BOLT 12 static invoice messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed for serialization. This is because it must mirror all the offer TLV records, including unknown ones, which aren't represented in the contents. Invoices may be created from an offer.
391bcbe
to
fb0d8e1
Compare
Squashed in the latest change with the following diff: diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index 77e2b1211..c6b6fa766 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -428,8 +428,7 @@ impl InvoiceContents {
}
fn fallbacks(&self) -> Vec<Address> {
- let chain =
- self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain());
+ let chain = self.chain();
self.fallbacks
.as_ref()
.map(|fallbacks| filter_fallbacks(chain, fallbacks)) |
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.
Please add a fuzzer for the new type in a followup.
Add a builder for creating static invoices for an offer. Building produces a semantically valid static invoice for the offer, which can then be signed with the key associated with the offer's signing pubkey.
bafe4ed
fb0d8e1
to
bafe4ed
Compare
Updated the outdated docs: diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index c6b6fa766..d0846b29a 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -82,12 +82,9 @@ struct InvoiceContents {
/// Builds a [`StaticInvoice`] from an [`Offer`].
///
-/// See [module-level documentation] for usage.
-///
/// [`Offer`]: crate::offers::offer::Offer
-/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
-/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+// TODO: add module-level docs and link here
pub struct StaticInvoiceBuilder<'a> {
offer_bytes: &'a Vec<u8>,
invoice: InvoiceContents, |
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.
Please add fuzzing in a followup :)
Will do, I added this to the tracking issue and will hopefully get to it soon. |
Defines an interface for static BOLT 12 invoices, based on lightning/bolts#1149.
Similar to single-use BOLT 12 invoices, except without a payment hash. Currently may only be created from
Offer
s.I have a local branch successfully paying one of these invoices, though with a lot of checks that will require pre-factors commented out, e.g.
PaymentPurpose
checks.Module-level docs