-
Notifications
You must be signed in to change notification settings - Fork 2.3k
bolt12: add Invoice codec and structural validators #10941
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
Open
bitromortac
wants to merge
5
commits into
lightningnetwork:master
Choose a base branch
from
bitromortac:2604-bolt12-1c
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c0b2ab1
bolt12: add TUint32 and invoice subtypes
bitromortac edf6249
bolt12: inject feature-bit catalogues into Offer and InvoiceRequest v…
bitromortac 262e9ab
bolt12: add Invoice struct and TLV codec
bitromortac 37df96e
bolt12: validate Invoice messages
bitromortac 0f49d8e
docs: add BOLT 12 invoice release notes
bitromortac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,36 @@ | ||
| package bolt12 | ||
|
|
||
| import ( | ||
| "encoding/binary" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "math" | ||
|
|
||
| "github.com/lightningnetwork/lnd/lnwire" | ||
| "github.com/lightningnetwork/lnd/tlv" | ||
| ) | ||
|
|
||
| // ErrTooManyChains is returned when offer_chains declares more entries than | ||
| // maxOfferChains. | ||
| var ErrTooManyChains = errors.New("offer_chains exceeds maxOfferChains") | ||
|
|
||
| // ErrNonMinimalFeatures is returned when a decoded feature vector is not | ||
| // canonically (minimally) encoded. | ||
| var ErrNonMinimalFeatures = errors.New("non-minimal feature vector encoding") | ||
|
|
||
| // ErrTooManyBlindedPayInfos is returned when decoded blinded_payinfo entries | ||
| // exceed maxBlindedPayInfos. | ||
| var ErrTooManyBlindedPayInfos = errors.New( | ||
| "invoice_blindedpay exceeds maxBlindedPayInfos", | ||
| ) | ||
|
|
||
| // ErrTooManyFallbackAddrs is returned when decoded fallback_address entries | ||
| // exceed maxFallbackAddrs. | ||
| var ErrTooManyFallbackAddrs = errors.New( | ||
| "invoice_fallbacks exceeds maxFallbackAddrs", | ||
| ) | ||
|
|
||
| const ( | ||
| // chainHashLen is the length of a chain hash (32 bytes). | ||
| chainHashLen = 32 | ||
|
|
@@ -20,6 +39,19 @@ const ( | |
| // check to prevent excessive memory allocation and is not a protocol | ||
| // limit but a local implementation choice. | ||
| maxOfferChains = 32 | ||
|
|
||
| // maxBlindedPayInfos caps decoded blinded_payinfo entries to prevent | ||
| // excessive allocation and validation cost. | ||
| maxBlindedPayInfos = 32 | ||
|
|
||
| // maxFallbackAddrs caps decoded fallback_address entries to prevent | ||
| // excessive allocation and validation cost. | ||
| maxFallbackAddrs = 32 | ||
|
|
||
| // maxFallbackAddrLen bounds the address bytes in a single fallback | ||
| // entry. The spec encodes the length as a uint16, so 65535 is the | ||
| // format's ceiling. | ||
| maxFallbackAddrLen = math.MaxUint16 | ||
| ) | ||
|
|
||
| // ChainsRecord holds one or more chain hashes for the offer_chains field. | ||
|
|
@@ -85,3 +117,305 @@ func decodeChainsRecord(r io.Reader, val any, _ *[8]byte, l uint64) error { | |
|
|
||
| return nil | ||
| } | ||
|
|
||
| // BlindedPayInfo holds the payment parameters for a blinded path, corresponding | ||
| // to the blinded_payinfo subtype. | ||
| type BlindedPayInfo struct { | ||
| FeeBaseMsat uint32 | ||
| FeeProportionalMillionths uint32 | ||
| CltvExpiryDelta uint16 | ||
| HtlcMinimumMsat uint64 | ||
| HtlcMaximumMsat uint64 | ||
|
|
||
| // Features is the relay feature bitmap for this blinded path, typed for | ||
| // consistency with the other BOLT 12 feature fields. | ||
| // | ||
| // WARNING: RawFeatureVector re-encodes to minimal length, so setting | ||
| // non-minimal feature bytes (trailing zeros) yields different wire | ||
| // bytes than were read and invalidates the invoice signature. | ||
| Features lnwire.RawFeatureVector | ||
| } | ||
|
|
||
| // BlindedPayInfos holds a list of BlindedPayInfo entries for the | ||
| // invoice_blindedpay field. | ||
| type BlindedPayInfos struct { | ||
| Infos []BlindedPayInfo | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a known issue as documented. I favored using the internal data structure for tlv mechanics over keeping the byte-wise representation. Added an error path to detect this explicitly. |
||
| } | ||
|
|
||
| // Record returns a TLV record for BlindedPayInfos. | ||
| // | ||
| // NOTE: This implements the tlv.RecordProducer interface. | ||
| func (bp *BlindedPayInfos) Record() tlv.Record { | ||
| return tlv.MakeDynamicRecord( | ||
| 0, bp, | ||
| func() uint64 { | ||
| return blindedPayInfosSize(bp) | ||
| }, | ||
| encodeBlindedPayInfos, decodeBlindedPayInfos, | ||
| ) | ||
| } | ||
|
|
||
| // blindedPayInfosSize returns the encoded byte length of all blinded_payinfo | ||
| // entries, used to size the dynamic TLV record. | ||
| func blindedPayInfosSize(bp *BlindedPayInfos) uint64 { | ||
| var size uint64 | ||
| for _, info := range bp.Infos { | ||
| // fee_base(4) + fee_prop(4) + cltv(2) + htlc_min(8) + | ||
| // htlc_max(8) + flen(2) + features. | ||
| size += 4 + 4 + 2 + 8 + 8 + 2 + | ||
| uint64(info.Features.SerializeSize()) | ||
| } | ||
|
|
||
| return size | ||
| } | ||
|
|
||
| // encodeBlindedPayInfos writes each blinded_payinfo entry in sequence: the | ||
| // fixed fee, cltv and htlc fields followed by a u16-length-prefixed feature | ||
| // vector. Entries are concatenated without a count prefix; the count is | ||
| // recovered on decode from the surrounding invoice_paths length. | ||
| func encodeBlindedPayInfos( | ||
| w io.Writer, val interface{}, buf *[8]byte) error { | ||
|
|
||
| bp, ok := val.(*BlindedPayInfos) | ||
| if !ok { | ||
| return fmt.Errorf("expected *BlindedPayInfos, got %T", val) | ||
| } | ||
|
|
||
| for _, info := range bp.Infos { | ||
| binary.BigEndian.PutUint32(buf[:4], info.FeeBaseMsat) | ||
| if _, err := w.Write(buf[:4]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint32( | ||
| buf[:4], info.FeeProportionalMillionths, | ||
| ) | ||
| if _, err := w.Write(buf[:4]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint16(buf[:2], info.CltvExpiryDelta) | ||
| if _, err := w.Write(buf[:2]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint64(buf[:8], info.HtlcMinimumMsat) | ||
| if _, err := w.Write(buf[:8]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint64(buf[:8], info.HtlcMaximumMsat) | ||
| if _, err := w.Write(buf[:8]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // flen is a u16, so guard the cast before framing the minimal | ||
| // feature bytes, mirroring encodeFallbackAddrs. | ||
| flen := info.Features.SerializeSize() | ||
| if flen > math.MaxUint16 { | ||
| return fmt.Errorf("features %d exceed limit %d", | ||
| flen, math.MaxUint16) | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint16(buf[:2], uint16(flen)) | ||
| if _, err := w.Write(buf[:2]); err != nil { | ||
| return err | ||
| } | ||
| if err := info.Features.EncodeBase256(w); err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
|
bitromortac marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // decodeBlindedPayInfos reads blinded_payinfo entries until the record bytes | ||
| // are exhausted. The entry count is capped at maxBlindedPayInfos to prevent | ||
| // excessive memory allocation and validation cost. | ||
| func decodeBlindedPayInfos( | ||
| r io.Reader, val interface{}, buf *[8]byte, l uint64) error { | ||
|
|
||
| bp, ok := val.(*BlindedPayInfos) | ||
| if !ok { | ||
| return fmt.Errorf("expected *BlindedPayInfos, got %T", val) | ||
| } | ||
|
|
||
| lr := &io.LimitedReader{R: r, N: int64(l)} | ||
|
|
||
| for lr.N > 0 { | ||
| if len(bp.Infos) >= maxBlindedPayInfos { | ||
| return ErrTooManyBlindedPayInfos | ||
| } | ||
|
|
||
| var info BlindedPayInfo | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:4]); err != nil { | ||
| return fmt.Errorf("read fee_base: %w", err) | ||
| } | ||
| info.FeeBaseMsat = binary.BigEndian.Uint32(buf[:4]) | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:4]); err != nil { | ||
| return fmt.Errorf("read fee_prop: %w", err) | ||
| } | ||
| info.FeeProportionalMillionths = binary.BigEndian.Uint32( | ||
| buf[:4], | ||
| ) | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:2]); err != nil { | ||
| return fmt.Errorf("read cltv_delta: %w", err) | ||
| } | ||
| info.CltvExpiryDelta = binary.BigEndian.Uint16(buf[:2]) | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:8]); err != nil { | ||
| return fmt.Errorf("read htlc_min: %w", err) | ||
| } | ||
| info.HtlcMinimumMsat = binary.BigEndian.Uint64(buf[:8]) | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:8]); err != nil { | ||
| return fmt.Errorf("read htlc_max: %w", err) | ||
| } | ||
| info.HtlcMaximumMsat = binary.BigEndian.Uint64(buf[:8]) | ||
|
|
||
| // flen then features, mirroring decodeFallbackAddrs: reject a | ||
| // length that overruns the remaining bytes before allocating. | ||
| // Decode into a constructed vector so its map is initialised. | ||
| if _, err := io.ReadFull(lr, buf[:2]); err != nil { | ||
| return fmt.Errorf("read flen: %w", err) | ||
| } | ||
| flen := binary.BigEndian.Uint16(buf[:2]) | ||
| if int64(flen) > lr.N { | ||
| return fmt.Errorf("flen %d exceeds remaining %d", | ||
| flen, lr.N) | ||
| } | ||
|
|
||
| fv := lnwire.NewRawFeatureVector() | ||
| if err := fv.DecodeBase256(lr, int(flen)); err != nil { | ||
| return fmt.Errorf("read features: %w", err) | ||
| } | ||
| if fv.SerializeSize() != int(flen) { | ||
| return ErrNonMinimalFeatures | ||
| } | ||
| info.Features = *fv | ||
|
|
||
| bp.Infos = append(bp.Infos, info) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // FallbackAddress represents an on-chain fallback address. | ||
| type FallbackAddress struct { | ||
| Version byte | ||
| Address []byte | ||
| } | ||
|
|
||
| // FallbackAddresses holds a list of fallback addresses for the | ||
| // invoice_fallbacks field. | ||
| type FallbackAddresses struct { | ||
| Addrs []FallbackAddress | ||
| } | ||
|
|
||
| // Record returns a TLV record for FallbackAddresses. | ||
| // | ||
| // NOTE: This implements the tlv.RecordProducer interface. | ||
| func (fa *FallbackAddresses) Record() tlv.Record { | ||
| return tlv.MakeDynamicRecord( | ||
| 0, fa, | ||
| func() uint64 { | ||
| return fallbackAddrsSize(fa) | ||
| }, | ||
| encodeFallbackAddrs, decodeFallbackAddrs, | ||
| ) | ||
| } | ||
|
|
||
| // fallbackAddrsSize returns the encoded byte length of all fallback_address | ||
| // entries, used to size the dynamic TLV record. | ||
| func fallbackAddrsSize(fa *FallbackAddresses) uint64 { | ||
| var size uint64 | ||
| for _, a := range fa.Addrs { | ||
| // version(1) + len(2) + address | ||
| size += 1 + 2 + uint64(len(a.Address)) | ||
| } | ||
|
|
||
| return size | ||
| } | ||
|
|
||
| // encodeFallbackAddrs writes each fallback_address entry as a version byte, a | ||
| // u16 address length and the raw address bytes, concatenated without a count | ||
| // prefix. | ||
| func encodeFallbackAddrs( | ||
| w io.Writer, val interface{}, buf *[8]byte) error { | ||
|
|
||
| fa, ok := val.(*FallbackAddresses) | ||
| if !ok { | ||
| return fmt.Errorf("expected *FallbackAddresses, got %T", val) | ||
| } | ||
|
|
||
| for i, a := range fa.Addrs { | ||
| if len(a.Address) > maxFallbackAddrLen { | ||
| return fmt.Errorf("fallback %d: address %d exceeds "+ | ||
| "limit %d", i, len(a.Address), | ||
| maxFallbackAddrLen) | ||
| } | ||
|
|
||
| buf[0] = a.Version | ||
| if _, err := w.Write(buf[:1]); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| binary.BigEndian.PutUint16(buf[:2], uint16(len(a.Address))) | ||
| if _, err := w.Write(buf[:2]); err != nil { | ||
| return err | ||
| } | ||
| if _, err := w.Write(a.Address); err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // decodeFallbackAddrs reads fallback_address entries until the record bytes are | ||
| // exhausted. The entry count is capped at maxFallbackAddrs to prevent | ||
| // excessive memory allocation and validation cost. | ||
| func decodeFallbackAddrs( | ||
| r io.Reader, val interface{}, buf *[8]byte, l uint64) error { | ||
|
|
||
| fa, ok := val.(*FallbackAddresses) | ||
| if !ok { | ||
| return fmt.Errorf("expected *FallbackAddresses, got %T", val) | ||
| } | ||
|
|
||
| lr := &io.LimitedReader{R: r, N: int64(l)} | ||
|
|
||
| for lr.N > 0 { | ||
| if len(fa.Addrs) >= maxFallbackAddrs { | ||
| return ErrTooManyFallbackAddrs | ||
| } | ||
|
|
||
| var a FallbackAddress | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:1]); err != nil { | ||
| return fmt.Errorf("read version: %w", err) | ||
| } | ||
| a.Version = buf[0] | ||
|
|
||
| if _, err := io.ReadFull(lr, buf[:2]); err != nil { | ||
| return fmt.Errorf("read addrlen: %w", err) | ||
| } | ||
| addrLen := binary.BigEndian.Uint16(buf[:2]) | ||
| if int64(addrLen) > lr.N { | ||
| return fmt.Errorf("addrlen %d exceeds remaining %d", | ||
| addrLen, lr.N) | ||
| } | ||
|
|
||
| a.Address = make([]byte, addrLen) | ||
| if _, err := io.ReadFull(lr, a.Address); err != nil { | ||
| return fmt.Errorf("read address: %w", err) | ||
| } | ||
|
|
||
| fa.Addrs = append(fa.Addrs, a) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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 believe it would be worth a change in the spec to only allow minimal encoded
featuresin all lightning types.This would be similar to what is being done with
channel_announcementandnode_announcement:lightning/bolts#1341 (review)