Skip to content
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

Experimental offer TLVs #3237

Merged
merged 19 commits into from
Nov 5, 2024

Conversation

jkczyz
Copy link
Contributor

@jkczyz jkczyz commented Aug 12, 2024

The BOLT12 spec defines experimental TLV ranges that are allowed in messages. Allow this range when parsing those messages and include those bytes in subsequent messages (i.e., experimental offer TLVs are included in an invreq after the signature). Account for these bytes when computing metadata, verifying messages, and constructing OfferIds.

Based on #3218.
Fixes #3168.

Copy link

codecov bot commented Aug 12, 2024

Codecov Report

Attention: Patch coverage is 90.74273% with 86 lines in your changes missing coverage. Please review.

Project coverage is 89.69%. Comparing base (605952c) to head (fda4b8d).
Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/util/ser.rs 18.18% 44 Missing and 10 partials ⚠️
lightning/src/offers/invoice.rs 96.25% 4 Missing and 7 partials ⚠️
lightning/src/offers/invoice_request.rs 96.77% 5 Missing and 4 partials ⚠️
lightning/src/offers/offer.rs 96.19% 5 Missing and 2 partials ⚠️
lightning/src/offers/refund.rs 94.68% 2 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3237      +/-   ##
==========================================
+ Coverage   89.68%   89.69%   +0.01%     
==========================================
  Files         126      127       +1     
  Lines      103306   107769    +4463     
  Branches   103306   107769    +4463     
==========================================
+ Hits        92648    96664    +4016     
- Misses       7945     8381     +436     
- Partials     2713     2724      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch 3 times, most recently from 1d15a6f to 55c6c56 Compare August 15, 2024 00:19
@jkczyz jkczyz marked this pull request as ready for review August 15, 2024 17:00
@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch 4 times, most recently from 3f5521c to 694533b Compare September 11, 2024 20:24
@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from 694533b to 1fc4d51 Compare September 16, 2024 21:49
@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from 1fc4d51 to b84d36b Compare October 2, 2024 16:50
@jkczyz
Copy link
Contributor Author

jkczyz commented Oct 2, 2024

Rebased

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

I find this pretty hard to review because several commits rewrite code that was rewritten in previous commits, making it hard to figure out what's wrong or what's just wrong until the last commit.

lightning/src/offers/static_invoice.rs Show resolved Hide resolved
lightning/src/offers/invoice.rs Show resolved Hide resolved
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
let experimental_bytes = Vec::new();

let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I find this really hard to read as of this commit - we're writing all the contents into bytes but then using an empty vec for the experimental set? Why is that not gonna result in TLVs in the wrong buffer? Later on there's a new set returned by as_tlv_stream but by doing that in a separate commit this is really hard to review.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At this point, we will fail parsing any Offer with TLVs in the experimental range. So offer_bytes will not contain any experimental TLVs.

It seemed easier to reason about by adding experimental_bytes in a dedicated commit. Otherwise, the change gets lost in the parsing commits (e.g., 927c477), which are large changesets.

Also note that we never use parts from as_tlv_stream that don't originated from us. We need to use the bytes instead because they may contain unknown odd TLVs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seemed easier to reason about by adding experimental_bytes in a dedicated commit. Otherwise, the change gets lost in the parsing commits (e.g., 927c477), which are large changesets.

Currently with things repeatedly changing in later commits I find myself reviewing the entire PR as one big diff because I can't keep track of what is going to stay the same and what's gonna change by the end, so I'm definitely okay with things being a part of larger commits as long as things aren't getting rewritten twice in the same PR :/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hrmm... I guess I can squash this commit into 927c477. The only other time this changes is trivially in d9a9e9d to add parsing support for the invoice request experimental range. Alternatively, I could move the logic added in 927c477 into this commit, which I've now done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added another fixup for UnsignedStaticInvoice.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Feel free to squash all the fixups and commits and I'll review again from the top anyway.

@@ -254,6 +254,7 @@ pub(super) struct TlvRecord<'a> {
type_bytes: &'a [u8],
// The entire TLV record.
pub(super) record_bytes: &'a [u8],
pub(super) end: usize,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks unused in this commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used in try_from for UnsignedBolt12Invoice and UnsignedInvoiceRequest.

@@ -492,17 +492,26 @@ where

impl UnsignedBolt12Invoice {
fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = EXPERIMENTAL_OFFER_TYPES;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be EXPERIMENTAL_OFFER_TYPES.start - EXPERIMENTAL_INVOICE_REQUEST_TYPES.end?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope... at this point we've only added support for parsing experimental offer TLVs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, which makes reviewing this patchset pretty confusing - we add things that don't really make sense as of the commit they're added in, and changed later to be correct. I'm not sure how to review that except to review the whole PR in one big diff-tree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't believe that is the case. Before this commit we didn't support parsing experimental types and after it we support experimental types in the offer range as indicated by this line and the commit message. In a later commit the range is expanded to support parsing experimental types in the invoice request range. Correctness is maintained the entire way since we will fail to parse anything that includes data in a yet-to-be supported experimental range.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This now is set to the ultimate range upfront.

) -> Result<PaymentId, ()> {
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = EXPERIMENTAL_OFFER_TYPES;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets not rename constants to make them less specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is later expanded to cover other experimental ranges as support is added.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, but at each turn the commits are confusing because they have a wrong value for where we're going. I find it really confusing to have to read some code, see one value, think hmm, that's not right, then go read the final state of the PR, realize no, indeed, it was not right, but its changed later, so I have no idea what the final code is actually going to look like.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This now is set to the ultimate range upfront.

@@ -1684,7 +1684,9 @@ mod tests {
message_paths: None,
},
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
ExperimentalOfferTlvStreamRef {},
ExperimentalOfferTlvStreamRef {
experimental_foo: None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rather than testing by overriding specific fields, can we just add support for passing TLV streams into the builders? We'll need/want to support passing custom types soon anyway (in #2829 or a followup, at least).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... right, though it seems there's a larger question as what sort of interface we should have when reading such TLVs. I think my assumption was we'd want to directly add TLVs that we support. If it's odd and we don't know it, we can ignore it. If it's even and we don't know it, we'd failed to parse. Unless instead we want ways to configure support kinda like custom messages. But that seems like a much larger change.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, I think we definitely should support passing custom TLVs, but I'm not sure it needs to be complicated. The logic in RecipientOnionFields to handle custom TLVs is pretty simple, and I'd imagined we'd duplicate basically that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we should do that in this PR. Sure, changing the builder isn't too difficult, but It's a significant change to the tlv_stream! macro to be able to support that. I'm not even sure if it can be done, TBH, without a substantial re-write since custom experimental TLVs may have types less than LDK-supported experimental TLVs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Alright, sounds good.

@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from b84d36b to 5f2991d Compare October 4, 2024 20:29
Copy link
Contributor Author

@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.

I find this pretty hard to review because several commits rewrite code that was rewritten in previous commits, making it hard to figure out what's wrong or what's just wrong until the last commit.

I don't believe that the case anywhere. The commits were written to maintain correctness from start to finish. Do you have a specific example in mind?

@@ -492,17 +492,26 @@ where

impl UnsignedBolt12Invoice {
fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = EXPERIMENTAL_OFFER_TYPES;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't believe that is the case. Before this commit we didn't support parsing experimental types and after it we support experimental types in the offer range as indicated by this line and the commit message. In a later commit the range is expanded to support parsing experimental types in the invoice request range. Correctness is maintained the entire way since we will fail to parse anything that includes data in a yet-to-be supported experimental range.

let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
let experimental_bytes = Vec::new();

let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hrmm... I guess I can squash this commit into 927c477. The only other time this changes is trivially in d9a9e9d to add parsing support for the invoice request experimental range. Alternatively, I could move the logic added in 927c477 into this commit, which I've now done.

@@ -1684,7 +1684,9 @@ mod tests {
message_paths: None,
},
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
ExperimentalOfferTlvStreamRef {},
ExperimentalOfferTlvStreamRef {
experimental_foo: None,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we should do that in this PR. Sure, changing the builder isn't too difficult, but It's a significant change to the tlv_stream! macro to be able to support that. I'm not even sure if it can be done, TBH, without a substantial re-write since custom experimental TLVs may have types less than LDK-supported experimental TLVs.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

This LGTM, I think. I started trying to review it per-commit again and got very very confused, so just gave up and reviewed the whole patchset, which is much easier, if also pretty annoying.

I do wonder if we're gonna end up wanting to change things a decent chunk when we go to actually support setting experimental entries in the structs. I assume we will, but probably not enough to merit changing this PR, though.

@@ -491,19 +492,32 @@ where

impl UnsignedBolt12Invoice {
fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't add things and then change them in later commits, if at all possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, I moved the constants ultimately used here up to this commit. Shouldn't affect the behavior since we don't support parsing messages with experimental TLV records at this point in the commit history.

const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..0;

let mut bytes = Vec::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unrelated to this PR (aside from it adding another vec), but rather than rebuilding the bytes here can we just store the invoice request (bytes) in the UnsignedBolt12Invoice and then do the work here in sign, building a single, new bytes with the invreq and invoice bytes in one go? Alternatively, we could iterate the bytes in sign to build the bytes without storing an extra vec here, but not sure its really worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly... I'd have to take a more thorough look to see if there's anything preventing it. Right now, this is also used to calculate the TaggedHash, so it may result in some duplication or need some more refactoring to avoid that.

It would also add a lifetime on the structs, which we may want to avoid? Forgetting if that will be problematic for bindings.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was thinking move InvoiceContents and store invreq_bytes as a Vec, rather than a slice (we currently always actually have an owned Vec at the callsite when calling here, afaict.

If we don't do this at a minimum we need a with_capacity here (overshooting a bit is fine, I think, this struct shouldn't be around long).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking move InvoiceContents and store invreq_bytes as a Vec, rather than a slice (we currently always actually have an owned Vec at the callsite when calling here, afaict.

Hmm... invreq_bytes comes from InvoiceBuilder as a slice from either &InvoiceRequest or &Refund, so we don't own it. We could copy it, but that leaves us with a Vec<u8> containing a both experimental and non-experimental ranges for which we need to insert a signature.

If we don't do this at a minimum we need a with_capacity here (overshooting a bit is fine, I think, this struct shouldn't be around long).

Done.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm... invreq_bytes comes from InvoiceBuilder as a slice from either &InvoiceRequest or &Refund, so we don't own it. We could copy it, but that leaves us with a Vec containing a both experimental and non-experimental ranges for which we need to insert a signature.

Oh sorry I had been looking at the deserialization logic and had that cached lol.


invoice_request_tlv_stream.write(&mut bytes).unwrap();

const EXPERIMENTAL_OFFER_TYPES: core::ops::Range<u64> = 0..0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't add things and then change them in later commits, if at all possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likewise here.

@@ -687,6 +687,12 @@ impl Offer {
self.contents.expects_quantity()
}

pub(super) fn tlv_stream_iter<'a>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

The commit message ends with "Using a common function ensures th", which looks cut off.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@@ -845,7 +845,7 @@ impl Bolt12Invoice {
(&refund.payer.0, REFUND_IV_BYTES_WITH_METADATA)
},
};
self.contents.verify(TlvStream::new(&self.bytes), metadata, key, iv_bytes, secp_ctx)
self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The commit message for this change says "Passing bytes directly to InvoiceContents::verify improves readability.", but this is also used to deal with the filtering in verify, which is useful in the coming commits, no? Would be nice if the commit message described this a bit more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. Added more context.

) -> Result<PaymentId, ()> {
const EXPERIMENTAL_TYPES: core::ops::Range<u64> = EXPERIMENTAL_OFFER_TYPES;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, but at each turn the commits are confusing because they have a wrong value for where we're going. I find it really confusing to have to read some code, see one value, think hmm, that's not right, then go read the final state of the PR, realize no, indeed, it was not right, but its changed later, so I have no idea what the final code is actually going to look like.

Comment on lines +497 to +499
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
Copy link
Collaborator

Choose a reason for hiding this comment

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

These constants are a bit confusingly named - they are actually only filters for invreq TLVs, but we're defining them in invoice.rs in a struct about invoices.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a clarifying comment here and in static_invoice.rs. They are local constants, so I don't think it's worth using overly elaborate names.

@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from fda4b8d to 0a116c2 Compare October 18, 2024 01:48
@jkczyz
Copy link
Contributor Author

jkczyz commented Oct 18, 2024

This LGTM, I think. I started trying to review it per-commit again and got very very confused, so just gave up and reviewed the whole patchset, which is much easier, if also pretty annoying.

2f9d44a and 906a6fb should address these concerns.

I do wonder if we're gonna end up wanting to change things a decent chunk when we go to actually support setting experimental entries in the structs. I assume we will, but probably not enough to merit changing this PR, though.

Arbitrary ones set by users, yes. Any we support should be the same as any TLV.

Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

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

Still need to look at the tests in more detail but I think this basically looks good!

lightning/src/offers/static_invoice.rs Show resolved Hide resolved
lightning/src/offers/invoice.rs Outdated Show resolved Hide resolved
lightning/src/offers/invoice.rs Show resolved Hide resolved
lightning/src/offers/invoice.rs Outdated Show resolved Hide resolved
lightning/src/offers/offer.rs Show resolved Hide resolved
lightning/src/offers/invoice.rs Outdated Show resolved Hide resolved
lightning/src/util/ser.rs Outdated Show resolved Hide resolved
@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch 2 times, most recently from 19f5e42 to afbe380 Compare October 22, 2024 20:35
@valentinewallace valentinewallace self-assigned this Oct 22, 2024
@jkczyz
Copy link
Contributor Author

jkczyz commented Nov 4, 2024

I added this assert and got a bunch of test failures. Are we intentionally overshooting on the invoice bytes capacity? It didn't seem like it to me but wanted to check.

diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
index 02eeaf4b3..285c79361 100644
--- a/lightning/src/offers/invoice.rs
+++ b/lightning/src/offers/invoice.rs
@@ -574,6 +574,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
                // Append the experimental bytes after the signature.
                WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();

+               assert_eq!(&$self.bytes.len(), &$self.bytes.capacity());
                Ok(Bolt12Invoice {
                        #[cfg(not(c_bindings))]
                        bytes: $self.bytes,

Yeah, SIGNATURE_TLV_RECORD_SIZE uses the size needed for TLV type 1000. It should be two bytes smaller for 240. We're more concerned about reallocating if the capacity is too small than allocating the exact number of bytes.

But for UnsignedBolt12Invoice this shouldn't happen as it isn't using that constant. Rather, it assumes that the signature for the invoice will be the same size as the request. Let me take a closer looks at what's happening.

@jkczyz
Copy link
Contributor Author

jkczyz commented Nov 4, 2024

But for UnsignedBolt12Invoice this shouldn't happen as it isn't using that constant. Rather, it assumes that the signature for the invoice will be the same size as the request. Let me take a closer looks at what's happening.

Ah, ok... some of the failure are because we directly modify bytes in the test to contain unknown / experimental TLVs. But there is a problem with creating an invoice for a refund. Since a refund doesn't have a signature, we do need to add SIGNATURE_TLV_RECORD_SIZE otherwise we won't have allocated enough bytes. Thanks for catching this! Simplest solution is probably just to unconditionally allocate SIGNATURE_TLV_RECORD_SIZE more bytes even if we over-allocate for the invoice for offer case.

@@ -528,6 +571,9 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
};
signature_tlv_stream.write(&mut $self.bytes).unwrap();

// Append the experimental bytes after the signature.
WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: would be more readable as $self.bytes.extend_from_slice(&$self.experimental_bytes).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in latest push.

diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
index 4f6c3a917..3b406daa7 100644
--- a/lightning/src/offers/invoice.rs
+++ b/lightning/src/offers/invoice.rs
@@ -573,7 +573,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
                signature_tlv_stream.write(&mut $self.bytes).unwrap();
 
                // Append the experimental bytes after the signature.
-               WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
+               $self.bytes.extend_from_slice(&$self.experimental_bytes);
 
                Ok(Bolt12Invoice {
                        #[cfg(not(c_bindings))]
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index 94c309844..a9beb4a2f 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -604,7 +604,7 @@ macro_rules! unsigned_invoice_request_sign_method { (
                signature_tlv_stream.write(&mut $self.bytes).unwrap();
 
                // Append the experimental bytes after the signature.
-               WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
+               $self.bytes.extend_from_slice(&$self.experimental_bytes);
 
                Ok(InvoiceRequest {
                        #[cfg(not(c_bindings))]
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index 228b9191a..dd1c191e6 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -343,7 +343,7 @@ impl UnsignedStaticInvoice {
                signature_tlv_stream.write(&mut self.bytes).unwrap();
 
                // Append the experimental bytes after the signature.
-               WithoutLength(&self.experimental_bytes).write(&mut self.bytes).unwrap();
+               self.bytes.extend_from_slice(&self.experimental_bytes);
 
                Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
        }

lightning/src/offers/invoice.rs Outdated Show resolved Hide resolved
lightning/src/offers/invoice.rs Show resolved Hide resolved
@TheBlueMatt
Copy link
Collaborator

I believe that fails not just for invoices but also invoice_requests and maybe others.

@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from 92201c5 to 1c88664 Compare November 4, 2024 20:00
@jkczyz
Copy link
Contributor Author

jkczyz commented Nov 4, 2024

I believe that fails not just for invoices but also invoice_requests and maybe others.

With the latest fixes, the following checks should pass except for the unknown / experimental cases where bytes is directly modified in those tests.

diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
index a201cdd58..e14ccce28 100644
--- a/lightning/src/offers/invoice.rs
+++ b/lightning/src/offers/invoice.rs
@@ -543,6 +543,7 @@ impl UnsignedBolt12Invoice {
                }
 
                experimental_invoice_tlv_stream.write(&mut experimental_bytes).unwrap();
+               assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
 
                let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
                let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
@@ -575,6 +576,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
                // Append the experimental bytes after the signature.
                WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
 
+               assert_eq!($self.bytes.len() + if $self.contents.is_for_offer() { 0 } else { 2 }, $self.bytes.capacity());
                Ok(Bolt12Invoice {
                        #[cfg(not(c_bindings))]
                        bytes: $self.bytes,
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index dd0c1f61d..af5ea401d 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -502,6 +502,7 @@ impl UnsignedInvoiceRequest {
                }
 
                experimental_invoice_request_tlv_stream.write(&mut experimental_bytes).unwrap();
+               assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
 
                let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
                let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
@@ -536,6 +537,7 @@ macro_rules! unsigned_invoice_request_sign_method { (
                // Append the experimental bytes after the signature.
                WithoutLength(&$self.experimental_bytes).write(&mut $self.bytes).unwrap();
 
+               assert_eq!($self.bytes.len() + 2, $self.bytes.capacity());
                Ok(InvoiceRequest {
                        #[cfg(not(c_bindings))]
                        bytes: $self.bytes,
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index 228b9191a..87deb3646 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -324,6 +324,7 @@ impl UnsignedStaticInvoice {
                }
 
                experimental_invoice_tlv_stream.write(&mut experimental_bytes).unwrap();
+               assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
 
                let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
                let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
@@ -345,6 +346,7 @@ impl UnsignedStaticInvoice {
                // Append the experimental bytes after the signature.
                WithoutLength(&self.experimental_bytes).write(&mut self.bytes).unwrap();
 
+               assert_eq!(self.bytes.len() + 2, self.bytes.capacity());
                Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
        }

@jkczyz
Copy link
Contributor Author

jkczyz commented Nov 4, 2024

Most recent changes:

diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
index 02eeaf4b3..4f6c3a917 100644
--- a/lightning/src/offers/invoice.rs
+++ b/lightning/src/offers/invoice.rs
@@ -122,7 +122,7 @@ use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_me
 #[cfg(test)]
 use crate::offers::invoice_macros::invoice_builder_methods_test;
 use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
-use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self};
+use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
 use crate::offers::nonce::Nonce;
 use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
 use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
@@ -512,6 +512,7 @@ impl UnsignedBolt12Invoice {
                let mut bytes = Vec::with_capacity(
                        invreq_bytes.len()
                                + invoice_tlv_stream.serialized_length()
+                               + if contents.is_for_offer() { 0 } else { SIGNATURE_TLV_RECORD_SIZE }
                                + experimental_invoice_tlv_stream.serialized_length(),
                );
 
@@ -530,10 +531,10 @@ impl UnsignedBolt12Invoice {
                        .range(EXPERIMENTAL_TYPES)
                        .peekable();
                let mut experimental_bytes = Vec::with_capacity(
-                       invreq_bytes.len()
+                       remaining_bytes.len()
                                - experimental_tlv_stream
                                        .peek()
-                                       .map_or(invreq_bytes.len(), |first_record| first_record.start)
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start)
                                + experimental_invoice_tlv_stream.serialized_length(),
                );
 
@@ -937,6 +938,13 @@ impl Hash for Bolt12Invoice {
 }
 
 impl InvoiceContents {
+       fn is_for_offer(&self) -> bool {
+               match self {
+                       InvoiceContents::ForOffer { .. } => true,
+                       InvoiceContents::ForRefund { .. } => false,
+               }
+       }
+
        /// Whether the original offer or refund has expired.
        #[cfg(feature = "std")]
        fn is_offer_or_refund_expired(&self) -> bool {
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index e0a6a8787..94c309844 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -560,10 +560,10 @@ impl UnsignedInvoiceRequest {
                        .range(EXPERIMENTAL_OFFER_TYPES)
                        .peekable();
                let mut experimental_bytes = Vec::with_capacity(
-                       offer.bytes.len()
+                       remaining_bytes.len()
                                - experimental_tlv_stream
                                        .peek()
-                                       .map_or(offer.bytes.len(), |first_record| first_record.start)
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start)
                                + experimental_invoice_request_tlv_stream.serialized_length(),
                );
 
diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs
index fa19040fb..228b9191a 100644
--- a/lightning/src/offers/static_invoice.rs
+++ b/lightning/src/offers/static_invoice.rs
@@ -312,10 +312,10 @@ impl UnsignedStaticInvoice {
                let mut experimental_tlv_stream =
                        TlvStream::new(remaining_bytes).range(EXPERIMENTAL_OFFER_TYPES).peekable();
                let mut experimental_bytes = Vec::with_capacity(
-                       offer_bytes.len()
+                       remaining_bytes.len()
                                - experimental_tlv_stream
                                        .peek()
-                                       .map_or(offer_bytes.len(), |first_record| first_record.start)
+                                       .map_or(remaining_bytes.len(), |first_record| first_record.start)
                                + experimental_invoice_tlv_stream.serialized_length(),
                );

@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from 1c88664 to 665a622 Compare November 4, 2024 20:26
@TheBlueMatt
Copy link
Collaborator

With the latest fixes, the following checks should pass except for the unknown / experimental cases where bytes is directly modified in those tests.

Is it possible to go ahead and add the checks as debug_asserts (with the tests that directly modify bytes set to just force the capacity to the expected value)?

When constructing UnsignedInvoiceRequest or UnsignedBolt12Invoice, use a
separate field for experimental TLV bytes. This allows for properly
inserting the signature TLVs before the experimental TLVs when signing.
Add a utility function for iterating over Offer TLV records contained in
any valid TLV stream bytes. Using a common function ensures that
experimental TLV records are included once they are supported.
Passing bytes directly to InvoiceContents::verify improves readability
as then a TlvStream for each TLV record range can be created from the
bytes instead of needing to clone the TlvStream upfront. In an upcoming
commit, the experimental TLV record range will utilize this.
Upcoming commits will allow parsing BOLT12 messages that include TLV
records in the experimental range. Include these ranges when verifying
messages since they will be included in the message bytes.
The BOLT12 spec defines an experimental TLV range that are allowed in
offer messages. Allow this range when parsing an offer and include those
bytes in any invoice requests. Also include those bytes when computing
an OfferId and verifying that an InvoiceRequest is for a valid Offer.
Offer metadata is generated from the offer TLVs and should included
those in the experimental range. When verifying invoice request and
invoice messages, these TLVs must be included. Similarly, OfferId
construction should included these TLVs as well. Modify the BOLT12
verification tests to cover these TLVs.
The BOLT12 spec defines an experimental TLV range that are allowed in
invoice_request messages. Allow this range when parsing an invoice
request and include those bytes in any invoice. Also include those bytes
when verifying that a Bolt12Invoice is for a valid InvoiceRequest.
Payer metadata is generated from the invreq TLVs and should included
those in the experimental range. When verifying invoice messages, these
TLVs must be included. Modify the BOLT12 verification tests to cover
them.
The BOLT12 spec defines an experimental TLV range that is allowed in
offer and invoice_request messages. The remaining TLV-space is for
experimental use in invoice messages. Allow this range when parsing an
invoice and include it when signing one.
@jkczyz jkczyz force-pushed the 2024-08-offers-experimental-tlvs branch from 665a622 to 169b260 Compare November 5, 2024 00:02
@jkczyz
Copy link
Contributor Author

jkczyz commented Nov 5, 2024

Is it possible to go ahead and add the checks as debug_asserts (with the tests that directly modify bytes set to just force the capacity to the expected value)?

Done.

@jkczyz jkczyz merged commit cb650a8 into lightningdevkit:main Nov 5, 2024
16 of 20 checks passed
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.

Support new experiemntal ranges for offer tlvs
3 participants