-
Notifications
You must be signed in to change notification settings - Fork 422
BOLT 12 offer encoding #1597
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 offer encoding #1597
Conversation
| /// For variable-length values within TLV record where the length is encoded as part of the record. | ||
| /// Used to prevent encoding the length twice. | ||
| #[derive(Clone)] | ||
| pub(crate) struct WithoutLength<T>(pub T); |
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.
We already have the vec_type TLV macro type for this - is there a reason we need this?
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.
Hmmm... if I use vec_type with impl_writeable_msg!, the Writeable implementation wants the field to be Vec<T> but the Readable implementation wants it to be Option<Vec<T>>.
If I use impl_writeable_tlv_based!, the variable is correctly unwrapped in the Readable implementation when initializing the struct. However, this macro won't work because it serializes the total length of the TLV stream, which isn't compliant with the 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.
Ok, so I whipped together a macro that is compliant with the spec. However, if a vec_type field is missing when parsing, it will be later serialized as present but with no values (i.e., a parsing-serialization roundtrip results in a different serialization).
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.
Hmmm... if I use vec_type with impl_writeable_msg!, the Writeable implementation wants the field to be Vec but the Readable implementation wants it to be Option<Vec>.
Yes, the way the init_tlv_field_var stuff works it creates an option, but it unwraps it once it gets to the struct creation phase, so that should be invisible from the pov of the user writing the macro? Not sure I quite understand the issue here.
Ok, so I whipped together a macro that is compliant with the spec. However, if a vec_type field is missing when parsing, it will be later serialized as present but with no values (i.e., a parsing-serialization roundtrip results in a different serialization).
Right, yea, we could do an optional_vec for that, I guess?
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.
Yes, the way the
init_tlv_field_varstuff works it creates an option, but it unwraps it once it gets to the struct creation phase, so that should be invisible from the pov of the user writing the macro? Not sure I quite understand the issue here.
The unwarp doesn't occur in impl_writeable_msg since init_tlv_based_struct_field is not used.
| $($tlvfield),* |
Maybe this was missed? Anyhow, need to make a new macro in order to remove the _empty field.
Right, yea, we could do an
optional_vecfor that, I guess?
We'd still have a problem with a parsing-serialization roundtrip if given a TLV record with length 0, FWIW. Not sure if such a thing would ever be written, but I've seen the presence of a TLV record have meaning elsewhere.
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.
Maybe this was missed? Anyhow, need to make a new macro in order to remove the _empty field.
Ah, yes, that is an oversight in impl_writeable_msg, it should be using init_tlv_based_struct_field
We'd still have a problem with a parsing-serialization roundtrip if given a TLV record with length 0, FWIW. Not sure if such a thing would ever be written, but I've seen the presence of a TLV record have meaning elsewhere.
Then we shouldn't write something like that in an optional_vec and reject it on the read end?
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.
Then we shouldn't write something like that in an
optional_vecand reject it on the read end?
I guess I'm just not sure if it is technically invalid.
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, we should probably get it specified in BOLT 12 then, no?
lightning/src/offers/mod.rs
Outdated
| /// An `offer` TLV stream without any semantic checks, apart from any checks performed when parsing | ||
| /// the underlying types. | ||
| struct OfferTlvStream { | ||
| _empty: (), |
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's the reason for this?
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.
impl_writeable_msg! doesn't like an empty set of non-TLV fields, and it seems impl_writeable_tlv_based! isn't compliant with the spec since it writes a length prefix.
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.
Heh, right, then let's add a new macro that doesn't require more stuff?
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.
Indeed, though note without using OfferTlvStream we'd be handwriting Readable and Writeable with read_tlv_fields! and write_tlv_fields! instead.
lightning/src/offers/mod.rs
Outdated
|
|
||
| use prelude::*; | ||
|
|
||
| /// An `offer` TLV stream without any semantic checks, apart from any checks performed when parsing |
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.
Hmmmm, is it not possible to have any of the fields be required? In general I'd prefer to do the required checks during deserialization so that we end up with a struct that we can expose that looks reasonable, rather than do the checks afterwards and convert a TlvStream struct into a ReasonableForHumans struct, copying it around.
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.
Hmmmm, is it not possible to have any of the fields be required?
Looks like the only required field is description. Not sure if we should necessarily make the field required here as the error would just be InvalidValue without indicating why.
In general I'd prefer to do the required checks during deserialization so that we end up with a struct that we can expose that looks reasonable, rather than do the checks afterwards and convert a TlvStream struct into a ReasonableForHumans struct, copying it around.
My plan was to wrap it in an Offer struct rather than copying it, and have accessors into the underlying OfferTlvStream. So the user would do something like "..".parse::<Offer>(), which would delegate to parsing with OfferTlvStream (which is private) before performing semantic checks.
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.
Looks like the only required field is description. Not sure if we should necessarily make the field required here as the error would just be InvalidValue without indicating why.
That's a separate issue though? If we want more errors out of the decoder, we can create those, lets not avoid using our decoder just because we want to add more features to the decoder.
My plan was to wrap it in an Offer struct rather than copying it, and have accessors into the underlying OfferTlvStream. So the user would do something like "..".parse::(), which would delegate to parsing with OfferTlvStream (which is private) before performing semantic checks.
I'm generally very much against this kind of approach - I'm strongly in the "write your datastructures right and the code writes itself" camp - the datastructures should thus generally not be able to represent an invalid state to the greatest extent possible. That implies doing checks during deserialization and refusing to create an object that is invalid, not deserializing and then checking later, leading to tons of unwraps in the accessors assuming the preconditions were checked correctly.
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's a separate issue though? If we want more errors out of the decoder, we can create those, lets not avoid using our decoder just because we want to add more features to the decoder.
I guess this is related to discussion below about code organization. If OfferTlvStream is thought to be an underlying implementation of Offer, then there are a few semantic checks related to field presence that would be required:
descriptionis present- either
node_idorpathsis present but not both refund_foris present only ifsend_invoiceis present
I wouldn't want to split these semantic checks across two layers. But if there is only an Offer struct, then all these checks would be written in a custom Readable, IIUC, where the first is performed by a read_tlv_fields! and the other two are handwritten.
I'm generally very much against this kind of approach - I'm strongly in the "write your datastructures right and the code writes itself" camp - the datastructures should thus generally not be able to represent an invalid state to the greatest extent possible. That implies doing checks during deserialization and refusing to create an object that is invalid, not deserializing and then checking later, leading to tons of unwraps in the accessors assuming the preconditions were checked correctly.
Hmmm... I think there's a tradeoff here. The code won't write itself in this case because we are required to adhere to the encoding as defined in BOLT 12, which doesn't have concepts of enums or structs with optional values, for instance. So the resulting Readable and Writeable implementations would be very procedural and prone to have inconsistencies.
On the other hand, the OfferTlvStream approach leads to a very declarative format, semantic checks are separated from type checks, and serialization is free and therefore less error prone (i.e., doesn't require deconstructing types into component TLV records). Given only one field is required, there should be only one unwrap; the optional field accessors are mostly of the nature: self.tlv_stream.field.as_ref(). Additionally, the wrapper Offer struct could only be created from a semantically valid OfferTlvStream since Offer's decoder would be responsible for performing semantic checks on OfferTlvStream. The drawback is accessors for complex types (i.e., enums, structs) may need to access more than one TLV record.
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, not using a wrapper means the semantic checks end up in the read method, rather than in a semantic_checks method. That said, the datastructure still forces the read code to do all relevant checks, by refusing to be able to represent (as many) invalid state(s as possible), it becomes much harder to write incorrect read code. Further, and more generally, it ensures the semantic check code is all in one place - in the read method - instead of spewed out between a semantic checks method, assumptions about the behavior thereof in accessors fetching fields, and exposed to users instead of simply exposing users a datastructure that they can instantiate, and only works for correct states.
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.
Will push a couple WIP commits showing the accessor API with wrapped types and separated semantic checks. I think I'd prefer to at least have the intermediary struct to enforce the TLV encoding, even if forgoing the wrapper approach. It gives a much better separation between type checks and semantic checsks. The OfferTlvStream can always be deconstructed into parts when converting to Offer. There's nothing performance critical here.
lightning/src/offers/mod.rs
Outdated
| impl Offer { | ||
| /// | ||
| pub fn description(&self) -> &String { | ||
| &self.tlv_stream.description.as_ref().unwrap().0 |
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 exactly this kind of thing that I find really gross. If there's a field that's required, it should be required, not an Option. It pushes the complexity into the implementation instead of the struct. I'm further not really sure why we can't just make it required in the TlvStream - why does it have to be an option and then unwraped?
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 exactly this kind of thing that I find really gross. If there's a field that's required, it should be required, not an
Option. It pushes the complexity into the implementation instead of the struct.
Agreed. Converting the TLV stream instead of wrapping would avoid this and is preferable.
I'm further not really sure why we can't just make it
requiredin the TlvStream - why does it have to be anoptionand then unwraped?
With the converting approach, I would prefer to keep it optional in the TLV stream (but required in Offer) so as to not mix error types across layers. Otherwise, missing a description would be a DecodeError while missing both node_id and paths would be a SemanticError.
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.
With the converting approach, I would prefer to keep it optional in the TLV stream (but required in Offer) so as to not mix error types across layers. Otherwise, missing a description would be a DecodeError while missing both node_id and paths would be a SemanticError.
Right, but there you're trying to work around limitations in the serialization framework by adding complexity further up...instead of improving the serialization framework to do what you want. If there's something you want from the serialization framework, lets add it, not work around 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.
Right, but there you're trying to work around limitations in the serialization framework by adding complexity further up...instead of improving the serialization framework to do what you want. If there's something you want from the serialization framework, lets add it, not work around it.
Hmmm... I'm not trying to get around any limitations of the serialization framework nor do I want anything more from it. It's working exactly as I expect it should: decoding a stream of TLV records as defined by each record's type. I don't see a compelling reason why decoding should include semantic checks. This is a standard separation of concerns.
Are you arguing that all semantic checks should be handled by the serialization framework? If not, what is the limiting principle as to what should be included there? It doesn't take long before business logic finds it's way in (e.g., quantity_min <= quantity_max -- assuming both are set -- or signature is valid for the offer given node_id or offer has not expired -- assuming std ). There are more complex semantics across amount, currency, and chains, which I haven't touched on yet. Excluding all those, supporting even simple semantics would add considerable complexity and maintenance burden to the serialization macros (e.g., if field A is set must not set field B, field A must only be set of field B is set, etc.).
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.
Yes, any semantic checks we want to do should happen at deserialization-time, at least from the perspective of the user. They can, of course, happen in separate functions if that makes more sense, but the user should call one public method and it should deserialize and do semantic checks. I don't think we really disagree here - you were already planning on doing this, AFAIU, just in a separate function that is invisible to the user, which is fine.
Separately, as much as possible without making things much too complicated, the datastructure should enforce as many semantic checks as possible, and once it does so, the deserialization logic will need to do the same in the same method, as it will be required to construct the object itself.
By the time we've finished reading an object we should be very confident that its sane and usable, and that way we massively reduce the risk of code downstream hitting unexpected cases, though it can/should also check. eg look at the 21-million-limit that was added to the BOLTs recently - this allows us explicitly to treat numbers greater than 21 million BTC as a hard deserialization failure, and generally we should.
In any case, the "limitations" I was referring to there is you made reference to different error types, which I read as "I want to have more specific error types for users to see more of what's going on", which I found to be reasonable, but isn't something that can currently be captured in the serialization framework - you get get an InvalidValue, generally.
lightning/src/offers/mod.rs
Outdated
| #[derive(Debug)] | ||
| struct BlindedPath { | ||
| blinding: PublicKey, | ||
| path: WithLength<Vec<OnionMessagePath>, u8>, |
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.
IIUC, I'd prefer have custom serialization methods and avoid WithLength leaking into the struct itself. However, guess this PR should soon be based on 1503 (+ maybe the serialization commit I have locally) and re-use blinded paths code from there (possibly moving it to its own module)?
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.
Yeah, I figured there may be some sort of consolidation here such that the interface is shared.
lightning/src/offers/mod.rs
Outdated
|
|
||
| /// | ||
| #[derive(Clone, Debug)] | ||
| pub struct 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.
Do you plan to move code out of mod?
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.
Yeah, I think so. Otherwise, mod.rs will get pretty big. I'll move it after squashing the fixups.
lightning/src/offers/mod.rs
Outdated
| /// An `offer` TLV stream without any semantic checks, apart from any checks performed when parsing | ||
| /// the underlying types. | ||
| #[derive(Debug)] | ||
| struct OfferTlvStream { |
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.
On initial pass, it does seem like it would be sufficient to separate the semantic checks into a separate method rather than a whole separate struct and allows us to get rid of the various conversion methods and other code?
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 may be possible to consolidate some of the conversions now that the code has evolved a bit. Let me play with that a bit. I kinda like having OfferTlvStream though as it removes duplication of type numbers across the read and write functions and enforces consistency with how integers and vectors are read/written.
lightning/src/offers/mod.rs
Outdated
|
|
||
| /// | ||
| #[derive(Clone, Debug)] | ||
| pub struct 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.
Do we plan to add a new method for this struct in this PR?
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.
Not sure yet. Originally, the idea was to add a Builder in a follow-up, but maybe that's not necessary -- though the syntax for it would be nice -- if any construction is semantically valid. That seems to be the case now outside of the signature, which itself would need to change as fields are updated. Maybe that's a good argument for keeping the signature separate and parsing as (Offer, Signature). Thoughts?
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 yeah we might want mutators. I like parsing as (Offer, Signature) and having a Offer.compute_signature() method, but not an actual struct field. Can discuss in the dev sync. May be useful to start a follow-up issue.
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.
Although note that this may require computing the offer id on the fly... hrmm
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, that's a good point about offer_id. Maybe it would be best to make Offers immutable and have a modifiable OfferConfig that can be used for easy mutation.
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 yeah we might want mutators. I like parsing as
(Offer, Signature)and having aOffer.compute_signature()method, but not an actual struct field. Can discuss in the dev sync. May be useful to start a follow-up issue.
Assuming you have the private key, which wouldn't be the case for a parsed offer. So, FWIW, you'd have to hang on to the signature if you want to re-serialize which is kinda yuck.
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.
Talked to Matt, I think my vote would be to go with the lightning-invoice builder pattern of constructing an UnsignedOffer then signing it off at the end to create an immutable Offer. Don't feel very strongly though if you have a different idea.
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.
Hmmm... when parsing, how would we know which type to use?
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 are you saying to parse as (UnsignedOffer, Option<Signature>)?
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 right. I think that points toward having signatures be optional in Offer. Talked to Matt, seems it's acceptable for offer_string.parse<Offer>() to return a read-only Offer with an optional signature. So we'll be giving up the mutability in favor of having an always-valid Offer.
lightning/src/offers/mod.rs
Outdated
| ), | ||
| }; | ||
|
|
||
| let (paths, node_id) = match self.destination.clone() { |
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.
@TheBlueMatt may be put off by Vec clones like these so we can do our best for devices that can't take too many memory allocations. In 1503 I found that disallowing extra vec allocations could sometimes change the code materially, so may be worth hashing out if any vecs need to go early on.
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 was an advantage of (previously) having Offer wrap OfferTlvStream (no conversion needed), whereas the current approach trades that for having Offer always being in a correct state. I think I'm convinced that the non-wrapping version is better. But it would be nice to get rid of this drawback while still having the advantages mentioned in https://github.com/lightningdevkit/rust-lightning/pull/1597/files#r934961472.
Let's consider the use cases:
- User scans QR code which gets parsed as an
OfferviaOfferTlvStream(no duplicate allocs) - User constructs an Offer programmatically to serialize as a string for a QR code (duplicate allocs)
The allocs could be removed if instead of using Display (which takes &self), we have a From conversion (which takes self) to OfferTlvStream or some other user-exposable type like OfferString or something. Though they may need to store the Offer as well (?), meaning they would need to copy it prior to serialization. Maybe serialization for QR code and in-memory storage would ultimately be different processes? We may need to put some more thought into how this may relate to offer_ids and to responding to invoice_requests. Let me re-read that part of the spec, but do let me know if you have any thoughts on this already.
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, to me it seems like it'd be nice to be able to keep it in-memory and be able to still serialize it (so kinda not a huge fan of From).
We may need to put some more thought into how this may relate to offer_ids and to responding to invoice_requests.
Not sure I'm getting how this could relate to offer_ids, is there some way to clarify?
For invoice_requests, I think it could make sense to have something like Offer.get_invoice_request(amount, quantity, etc) -> Result<InvoiceRequest, Error>. I was originally thinking this would live in OnionMessenger but it probably makes more sense in this module?
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.
Pushed a couple commits at the end for signature verification and (related) offer_id computation. Another point to consider is that -- while the offer_id is computed as part of signature verification and can be stored in Offer when parsed -- for programmatic constructions, the offer_id still needs to be computed by converting to the serialized TLV encoding either on-demand or at construction time. So we're going to get hit by allocations there it would seem regardless. Probably could reduce the number of allocations in merkle root computation, 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.
Not sure I'm getting how this could relate to
offer_ids, is there some way to clarify?
Mostly pointing out when responding to an invoice_request, we need to know if we actually know about an Offer with the corresponding offer_id from the request. So we'll need some way of determining this and responding with an appropriate invoice. I guess only tangentially related to allocs.
For
invoice_requests, I think it could make sense to have something likeOffer.get_invoice_request(amount, quantity, etc) -> Result<InvoiceRequest, Error>. I was originally thinking this would live inOnionMessengerbut it probably makes more sense in this module?
Yeah, that was going to be the next step here.
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.
Pushed a couple commits at the end for signature verification and (related)
offer_idcomputation. Another point to consider is that -- while theoffer_idis computed as part of signature verification and can be stored inOfferwhen parsed -- for programmatic constructions, theoffer_idstill needs to be computed by converting to the serialized TLV encoding either on-demand or at construction time. So we're going to get hit by allocations there it would seem regardless. Probably could reduce the number of allocations in merkle root computation, though.
Would have to check it out more closely, mostly just wanted to note this as something to consider.
Mostly pointing out when responding to an invoice_request, we need to know if we actually know about an Offer with the corresponding offer_id from the request. So we'll need some way of determining this and responding with an appropriate invoice. I guess only tangentially related to allocs.
I think we'll need to add something similar to the inbound_payment module but for inbound invoices. Then we can do Offer::verify("payment_secret", &invoice, &inbound_invoice_secret). To be clear, the "payment_secret" comes in the encrypted_data we encoded for ourselves when we constructed the blinded route, so not 100% on where verify lives.
lightning/src/offers/mod.rs
Outdated
| #[derive(Clone, Debug)] | ||
| pub struct Offer { | ||
| id: sha256::Hash, | ||
| chains: Option<Vec<BlockHash>>, |
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.
IIRC rusty had said he wanted to drop the vec and just have one chain.
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 do recall that but it seems supporting both the main chain and any side chains with the same offer is a use case he wants to support as per: lightning/bolts#798 (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.
Responded there.
lightning/src/offers/mod.rs
Outdated
| }, | ||
| /// An amount of currency specified using ISO 4712. | ||
| Currency { | ||
| /// An ISO 4712 three-letter currency code (e.g., USD). |
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 its three letters why is it on heap? :p
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.
Note that it's already been allocated when the TLV stream was parsed. Extracting characters from strings in Rust without an intermediary Vec is kinda gross, too. Either:
let mut chars = iso4217_code.chars();
let iso4217_code = [chars.next().unwrap(), chars.next().unwrap(), chars.next().unwrap()];Or in 1.55:
let mut chars = iso4217_code.chars();
[(); ISO4217_CODE_LEN].map(|_| chars.next().unwrap())Happy to change it if you feel strongly. Though note that crates like iso_4217 work in terms of str if we ever need to interface with 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.
Can't we store it as a [u8; 3] and use https://doc.rust-lang.org/std/str/fn.from_utf8.html in the getter? Is the string not guaranteed to be ASCII?
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.
Yeah, good point. Though it's inside of an enum variant. Not too important at the moment, but changed to [u8; 3] anyway.
lightning/src/offers/mod.rs
Outdated
|
|
||
| /// An offer parsed from a bech32-encoded string as a TLV stream and the corresponding bytes. The | ||
| /// latter is used for signature verification. | ||
| struct ParsedOffer(OfferTlvStream, Vec<u8>); |
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 really confused here now - so when we're parsing we first read some TLV fields from bytes, then we copy the bytes into this, with the TLV fields moved into it, then we do some analysis of the TLV fields and copy them into a higher-level struct? I'd prefer we just copy the 10 lines of TLV-read code into the top of the Offer read method and call it a day, all the copying around of things just seems to add more code and boilerplate and I'm still not sure how it makes anything more readable. Can you help me understand why all the boilerplate here is a simpler design?
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 really confused here now - so when we're parsing we first read some TLV fields from bytes, then we copy the bytes into this, with the TLV fields moved into it,
The bytes are not copied -- they are moved just as the TLV fields are.
then we do some analysis of the TLV fields and copy them into a higher-level struct?
I believe they are moved into the higher level struct since OfferTlvStream is a value type -- I'd get compiler errors if I tried to reuse any part of the destructured OfferTlvStream after it had been moved.
I'd prefer we just copy the 10 lines of TLV-read code into the top of the
Offerread method and call it a day, all the copying around of things just seems to add more code and boilerplate and I'm still not sure how it makes anything more readable. Can you help me understand why all the boilerplate here is a simpler design?
I'm not sure how that is materially any different. We'd still need to declare variables prior to those lines, at least some with explicit types. And then repeat the types in the write code, ensuring the vector and integer types match and are serialized correctly rather than relying on the compiler to enforce it.
Additionally, that would require changing the return type to ParseError (in order to wrap DecodeError) even though all the other errors are SemanticError. And then all the error returns would need to be wrapped or littered with into(). It would also be possible to return ParseError variants which shouldn't be possible at that level.
I pushed a version to show what this looks like. I personally find it much less readable in addition to the other problems that it introduces as mentioned above. Moreover, the OfferTlvStream version is much more compact and self-describing, IMHO. I don't see how it introduces boilerplate and more code compared to the alternative.
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.
Discussed this offline yesterday. I pushed a new HEAD commit after reverting the non-OfferTlvStream version. The new version avoids any clones when writing. It also hides the WithoutLength and HighZeroBytesDroppedVarInt types from the OfferTlvStream struct definition and consolidates the field and TLV type definitions.
The conversions still need to be aware of the wrapper types, though. Maybe there's a neat way to avoid that with type coercions?
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.
Added some From implementations to hide all the WithoutLength and HighZeroBytesDroppedVarInt wrappers.
lightning/src/offers/mod.rs
Outdated
| #[derive(Clone, Debug)] | ||
| pub enum Destination { | ||
| /// | ||
| NodeId(XOnlyPublicKey), |
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.
IIUC this is going away, maybe we need to sync with rusty before we move forward too much here.
lightning/src/ln/features.rs
Outdated
| } | ||
| } | ||
|
|
||
| impl_feature_tlv_value_write!(ChannelTypeFeatures); |
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.
s/_value//, otherwise reads as "type length value value"
lightning/src/offers/mod.rs
Outdated
|
|
||
| /// | ||
| #[derive(Clone, Debug)] | ||
| pub struct 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.
Ah yeah we might want mutators. I like parsing as (Offer, Signature) and having a Offer.compute_signature() method, but not an actual struct field. Can discuss in the dev sync. May be useful to start a follow-up issue.
lightning/src/offers/mod.rs
Outdated
| ), | ||
| }; | ||
|
|
||
| let (paths, node_id) = match self.destination.clone() { |
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, to me it seems like it'd be nice to be able to keep it in-memory and be able to still serialize it (so kinda not a huge fan of From).
We may need to put some more thought into how this may relate to offer_ids and to responding to invoice_requests.
Not sure I'm getting how this could relate to offer_ids, is there some way to clarify?
For invoice_requests, I think it could make sense to have something like Offer.get_invoice_request(amount, quantity, etc) -> Result<InvoiceRequest, Error>. I was originally thinking this would live in OnionMessenger but it probably makes more sense in this module?
953fd74 to
2ccc166
Compare
|
Had some offline conversation about this one and I'm happy with where we ended up here. I'm not sure there's a lot more to comment on specific about the PR, though there's some upstream stuff to work out with Rusty on formatting and structure. Let me know if you want more review on this PR specifically until we resolve those. |
53024ae to
21c51c5
Compare
|
|
||
| pub(super) fn tagged_hash<T: AsRef<[u8]>>(tag: sha256::Hash, msg: T) -> sha256::Hash { | ||
| let mut engine = sha256::Hash::engine(); | ||
| engine.input(tag.as_ref()); |
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.
Rather than re-hashing the tag twice for each leaf, we should just have a SHA256 engine in root_hash for each of the tags, clone and then write the extra msg bytes and finish.
0ca166e to
75b8574
Compare
|
@TheBlueMatt @valentinewallace The latest few commits implement |
|
|
||
| /// | ||
| pub struct InvoiceRequest { | ||
| bytes: Vec<u8>, |
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.
Wait, do we need to store the original bytes in the InvoiceRequest as well? I thought that was only going to be required in the offer to echo it back in the InvoiceRequest?
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.
When responding to an invoice_request with an invoice, fields from both the offer and the invoice_request must be reflected.
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.
We could avoid duplicating the field by always storing the bytes in Offer instead of using an empty Vec there. I wasn't too thrilled about having it empty depending on which is the outermost message. Likewise, for when parsing an Invoice. And then implement AsRef<[u8]> on all messages to pull it from the Offer.
| (89, payer_note: String), | ||
| }); | ||
|
|
||
| impl Bech32Encode for InvoiceRequest { |
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.
Wait, why would one want to bech32-encode an InvoiceRequest? Isn't it basically only ever an onion message?
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.
The most recent proposal to the draft is to drop send_invoice from offer and instead bech32 encode an invoice_request. See rustyrussell/bolts#11.
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, so we won't really have an offer at all? Its just an invoice request with some reduced fields?
f4e17b4 to
1b2e9be
Compare
|
Ok, so most of the recent changes have been to keep up with rustyrussell/bolts#11 and rustyrussell/bolts#7. Probably best to look at all files at once. In particular, changes include reflecting fields properly when building and |
|
Also, note that this doesn't yet have a builder for the " |
| // unknown TLV records, which are not stored in `OfferContents`. | ||
| let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream, _) = | ||
| self.invoice_request.as_tlv_stream(); | ||
| let offer_bytes = WithoutLength(&self.offer.bytes); |
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.
Note, this may contain custom records after the signature range. So actually, need to break this into two sequences of bytes. When building an invoice from an invoice_request will need three sequences 0-160 (offer and invoice_request), 240-1000 (signatures to strip), and 1000+ (custom records).
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.
and 1000+ (custom records).
Hm I can't find this in the spec, am I missing it? Commented on the PR: rustyrussell/bolts#7 (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.
Ah, right. As written it does not support it. I suppose unused types in the 0-240 range could be custom, but presumably we may want a dedicated range.
48a1a11 to
8d8f554
Compare
ce43cd9 to
0ccfe84
Compare
0ccfe84 to
74f0102
Compare
Codecov ReportBase: 90.80% // Head: 90.48% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #1597 +/- ##
==========================================
- Coverage 90.80% 90.48% -0.33%
==========================================
Files 89 94 +5
Lines 47963 50227 +2264
Branches 47963 50227 +2264
==========================================
+ Hits 43554 45448 +1894
- Misses 4409 4779 +370
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
74f0102 to
b8893a1
Compare
9cb50ae to
5200268
Compare
20b2705 to
53f9e89
Compare
Add common bech32 parsing for BOLT 12 messages. The encoding is similar to bech32 only without a checksum and with support for continuing messages across multiple parts. Messages implementing Bech32Encode are parsed into a TLV stream, which is converted to the desired message content while performing semantic checks. Checking after conversion allows for more elaborate checks of data composed of multiple TLV records and for more meaningful error messages. The parsed bytes are also saved to allow creating messages with mirrored data, even if TLV records are unknown.
Test semantic errors when parsing offer bytes.
53f9e89 to
2157549
Compare
BOLT 12 messages are limited to a range of TLV record types. Refactor decode_tlv_stream into a decode_tlv_stream_range macro for limiting which types are parsed. Requires a SeekReadable trait for rewinding when a type outside of the range is seen. This allows for composing TLV streams of different ranges. Updates offer parsing accordingly and adds a test demonstrating failure if a type outside of the range is included.
Define an interface for BOLT 12 `invoice_request` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed when constructing an `invoice` message. This is because it must mirror all the `offer` and `invoice_request` TLV records, including unknown ones, which aren't represented in the contents. The contents will be used in `invoice` messages to avoid duplication. Some fields while required in a typical user-pays-merchant flow may not be necessary in the merchant-pays-user flow (e.g., refund, ATM).
BOLT 12 uses Schnorr signatures for signing offers messages, which need to be serialized.
Offers uses a merkle root hash construction for signature calculation and verification. Add a submodule implementing this so that it can be used when parsing and signing invoice_request and invoice messages.
When reading an offer, an `invoice_request` message is sent over the wire. Implement Writeable for encoding the message and TryFrom for decoding it by defining in terms of TLV streams. These streams represent content for the payer metadata (0), reflected `offer` (1-79), `invoice_request` (80-159), and signature (240).
Implement Bech32Encode for InvoiceRequest, which supports creating and parsing QR codes for the merchant-pays-user (e.g., refund, ATM) flow.
2157549 to
cd8cafb
Compare
|
I assume this should be closed now? |
|
Yeah, everything made it in other PRs. Closing. |
WIP on encoding and parsing
offermessages.TODO:
BlindedPathencoding