• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 18914042383

29 Oct 2025 03:53PM UTC coverage: 54.594%. First build
18914042383

Pull #10089

github

web-flow
Merge 5a7e61d97 into e8a486fa6
Pull Request #10089: Onion message forwarding

93 of 590 new or added lines in 10 files covered. (15.76%)

110416 of 202249 relevant lines covered (54.59%)

21682.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lnwire/onion_msg_payload.go
1
package lnwire
2

3
import (
4
        "bytes"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "sort"
9

10
        "github.com/btcsuite/btcd/btcec/v2"
11
        "github.com/lightningnetwork/lnd/tlv"
12
)
13

14
const (
15
        // finalHopPayloadStart is the inclusive beginning of the tlv type
16
        // range that is reserved for payloads for the final hop.
17
        finalHopPayloadStart tlv.Type = 64
18

19
        // replyPathType is a record for onion messaging reply paths.
20
        replyPathType tlv.Type = 2
21

22
        // encryptedDataTLVType is a record containing encrypted data for
23
        // message recipient.
24
        encryptedDataTLVType tlv.Type = 4
25

26
        // InvoiceRequestNamespaceType is a record containing the sub-namespace
27
        // of tlvs that request invoices for offers.
28
        InvoiceRequestNamespaceType tlv.Type = 64
29

30
        // InvoiceNamespaceType is a record containing the sub-namespace of
31
        // tlvs that describe an invoice.
32
        InvoiceNamespaceType tlv.Type = 66
33

34
        // InvoiceErrorNamespaceType is a record containing the sub-namespace of
35
        // tlvs that describe an invoice error.
36
        InvoiceErrorNamespaceType tlv.Type = 68
37
)
38

39
var (
40
        // ErrNotFinalPayload is returned when a final hop payload is not
41
        // within the correct range.
42
        ErrNotFinalPayload = errors.New("final hop payloads type should be " +
43
                ">= 64")
44

45
        // ErrNoHops is returned when we handle a reply path that does not
46
        // have any hops (this makes no sense).
47
        ErrNoHops = errors.New("reply path requires hops")
48
)
49

50
// OnionMessagePayload contains the contents of an onion message payload.
51
type OnionMessagePayload struct {
52
        // ReplyPath contains a blinded path that can be used to respond to an
53
        // onion message.
54
        ReplyPath *ReplyPath
55

56
        // EncryptedData contains encrypted data for the recipient.
57
        EncryptedData []byte
58

59
        // FinalHopPayloads contains any tlvs with type > 64 that
60
        FinalHopPayloads []*FinalHopPayload
61
}
62

63
// NewOnionMessage creates a new OnionMessage.
NEW
64
func NewOnionMessagePayload() *OnionMessagePayload {
×
NEW
65
        return &OnionMessagePayload{}
×
NEW
66
}
×
67

68
// Encode encodes an onion message's final payload.
NEW
69
func (o *OnionMessagePayload) Encode() ([]byte, error) {
×
NEW
70
        var records []tlv.Record
×
NEW
71

×
NEW
72
        if o.ReplyPath != nil {
×
NEW
73
                records = append(records, o.ReplyPath.record())
×
NEW
74
        }
×
75

NEW
76
        if len(o.EncryptedData) != 0 {
×
NEW
77
                record := tlv.MakePrimitiveRecord(
×
NEW
78
                        encryptedDataTLVType, &o.EncryptedData,
×
NEW
79
                )
×
NEW
80
                records = append(records, record)
×
NEW
81
        }
×
82

NEW
83
        for _, finalHopPayload := range o.FinalHopPayloads {
×
NEW
84
                if err := finalHopPayload.Validate(); err != nil {
×
NEW
85
                        return nil, err
×
NEW
86
                }
×
87

88
                // Create a primitive record that just writes the final hop
89
                // payload's bytes directly. The creating function should have
90
                // encoded the value correctly.
NEW
91
                record := tlv.MakePrimitiveRecord(
×
NEW
92
                        finalHopPayload.TLVType, &finalHopPayload.Value,
×
NEW
93
                )
×
NEW
94
                records = append(records, record)
×
95
        }
96

97
        // Sort our records just in case the final hop payload records were
98
        // provided in the incorrect order.
NEW
99
        tlv.SortRecords(records)
×
NEW
100

×
NEW
101
        stream, err := tlv.NewStream(records...)
×
NEW
102
        if err != nil {
×
NEW
103
                return nil, fmt.Errorf("new stream: %w", err)
×
NEW
104
        }
×
105

NEW
106
        b := new(bytes.Buffer)
×
NEW
107
        if err := stream.Encode(b); err != nil {
×
NEW
108
                return nil, fmt.Errorf("encode stream: %w", err)
×
NEW
109
        }
×
110

NEW
111
        return b.Bytes(), nil
×
112
}
113

114
// Decode decodes an onion message's payload.
115
func (o *OnionMessagePayload) Decode(r io.Reader) (*OnionMessagePayload,
NEW
116
        map[tlv.Type][]byte, error) {
×
NEW
117

×
NEW
118
        var (
×
NEW
119
                invoicePayload = &FinalHopPayload{
×
NEW
120
                        TLVType: InvoiceNamespaceType,
×
NEW
121
                }
×
NEW
122

×
NEW
123
                invoiceErrorPayload = &FinalHopPayload{
×
NEW
124
                        TLVType: InvoiceErrorNamespaceType,
×
NEW
125
                }
×
NEW
126

×
NEW
127
                invoiceRequestPayload = &FinalHopPayload{
×
NEW
128
                        TLVType: InvoiceRequestNamespaceType,
×
NEW
129
                }
×
NEW
130
        )
×
NEW
131
        // Create a non-nil entry so that we can directly decode into it.
×
NEW
132
        o.ReplyPath = &ReplyPath{}
×
NEW
133

×
NEW
134
        records := []tlv.Record{
×
NEW
135
                o.ReplyPath.record(),
×
NEW
136
                tlv.MakePrimitiveRecord(
×
NEW
137
                        encryptedDataTLVType, &o.EncryptedData,
×
NEW
138
                ),
×
NEW
139
                // Add a record for invoice request sub-namespace so that we
×
NEW
140
                // won't fail on the even tlv - reasoning above.
×
NEW
141
                tlv.MakePrimitiveRecord(
×
NEW
142
                        InvoiceRequestNamespaceType,
×
NEW
143
                        &invoiceRequestPayload.Value,
×
NEW
144
                ),
×
NEW
145
                // Add records to read invoice and invoice errors sub-namespaces
×
NEW
146
                // out. Although this is technically one of our "final hop
×
NEW
147
                // payload" tlvs, it is an even value, so we need to include it
×
NEW
148
                // as a known tlv here, or decoding will fail. We decode
×
NEW
149
                // directly into a final hop payload, so that we can just add it
×
NEW
150
                // if present later.
×
NEW
151
                tlv.MakePrimitiveRecord(
×
NEW
152
                        InvoiceNamespaceType,
×
NEW
153
                        &invoicePayload.Value,
×
NEW
154
                ),
×
NEW
155
                tlv.MakePrimitiveRecord(
×
NEW
156
                        InvoiceErrorNamespaceType,
×
NEW
157
                        &invoiceErrorPayload.Value,
×
NEW
158
                ),
×
NEW
159
        }
×
NEW
160

×
NEW
161
        stream, err := tlv.NewStream(records...)
×
NEW
162
        if err != nil {
×
NEW
163
                return nil, nil, fmt.Errorf("new stream: %w", err)
×
NEW
164
        }
×
165

NEW
166
        tlvMap, err := stream.DecodeWithParsedTypesP2P(r)
×
NEW
167
        if err != nil {
×
NEW
168
                return nil, tlvMap, fmt.Errorf("decode stream: %w", err)
×
NEW
169
        }
×
170

171
        // If our reply path wasn't populated, replace it with a nil entry.
NEW
172
        if _, ok := tlvMap[replyPathType]; !ok {
×
NEW
173
                o.ReplyPath = nil
×
NEW
174
        }
×
175

176
        // Once we're decoded our message, we want to also include any tlvs
177
        // that are intended for the final hop's payload which we may not have
178
        // recognized. We'll just directly read these out and allow higher
179
        // application layers to deal with them.
NEW
180
        for tlvType, tlvBytes := range tlvMap {
×
NEW
181
                // Skip any tlvs that are not in our range.
×
NEW
182
                if tlvType < finalHopPayloadStart {
×
NEW
183
                        continue
×
184
                }
185

186
                // Skip any tlvs that have been recognized in our decoding (a
187
                // zero entry means that we recognized the entry).
NEW
188
                if len(tlvBytes) == 0 {
×
NEW
189
                        continue
×
190
                }
191

192
                // Add the payload to our message's final hop payloads.
NEW
193
                payload := &FinalHopPayload{
×
NEW
194
                        TLVType: tlvType,
×
NEW
195
                        Value:   tlvBytes,
×
NEW
196
                }
×
NEW
197

×
NEW
198
                o.FinalHopPayloads = append(
×
NEW
199
                        o.FinalHopPayloads, payload,
×
NEW
200
                )
×
201
        }
202

203
        // If we read out an invoice, invoice error or invoice request tlv
204
        // sub-namespace, add it to our set of final payloads. This value won't
205
        // have been added in the loop above, because we recognized the TLV so
206
        // len(tlvMap[invoiceType].tlvBytes) will be zero (thus, skipped above).
NEW
207
        if _, ok := tlvMap[InvoiceNamespaceType]; ok {
×
NEW
208
                o.FinalHopPayloads = append(
×
NEW
209
                        o.FinalHopPayloads, invoicePayload,
×
NEW
210
                )
×
NEW
211
        }
×
212

NEW
213
        if _, ok := tlvMap[InvoiceErrorNamespaceType]; ok {
×
NEW
214
                o.FinalHopPayloads = append(
×
NEW
215
                        o.FinalHopPayloads, invoiceErrorPayload,
×
NEW
216
                )
×
NEW
217
        }
×
218

NEW
219
        if _, ok := tlvMap[InvoiceRequestNamespaceType]; ok {
×
NEW
220
                o.FinalHopPayloads = append(
×
NEW
221
                        o.FinalHopPayloads, invoiceRequestPayload,
×
NEW
222
                )
×
NEW
223
        }
×
224

225
        // Iteration through maps occurs in random order - sort final hop
226
        // payloads in ascending order to make this decoding function
227
        // deterministic.
NEW
228
        sort.SliceStable(o.FinalHopPayloads, func(i, j int) bool {
×
NEW
229
                return o.FinalHopPayloads[i].TLVType <
×
NEW
230
                        o.FinalHopPayloads[j].TLVType
×
NEW
231
        })
×
232

NEW
233
        return o, tlvMap, nil
×
234
}
235

236
// FinalHopPayload contains values reserved for the final hop, which are just
237
// directly read from the tlv stream.
238
type FinalHopPayload struct {
239
        // TLVType is the type for the payload.
240
        TLVType tlv.Type
241

242
        // Value is the raw byte value read for this tlv type. This field is
243
        // expected to contain "sub-tlv" namespaces, and will require further
244
        // decoding to be used.
245
        Value []byte
246
}
247

248
// ValidateFinalPayload returns an error if a tlv is not within the range
249
// reserved for final papyloads.
NEW
250
func ValidateFinalPayload(tlvType tlv.Type) error {
×
NEW
251
        if tlvType < finalHopPayloadStart {
×
NEW
252
                return fmt.Errorf("%w: %v", ErrNotFinalPayload, tlvType)
×
NEW
253
        }
×
254

NEW
255
        return nil
×
256
}
257

258
// Validate performs validation of items added to the final hop's payload in an
259
// onion. This function does not validate payload length to allow "marker-tlvs"
260
// that have no body.
NEW
261
func (f *FinalHopPayload) Validate() error {
×
NEW
262
        if err := ValidateFinalPayload(f.TLVType); err != nil {
×
NEW
263
                return err
×
NEW
264
        }
×
265

NEW
266
        return nil
×
267
}
268

269
// ReplyPath is a blinded path used to respond to onion messages.
270
type ReplyPath struct {
271
        // FirstNodeID is the pubkey of the first node in the reply path.
272
        FirstNodeID *btcec.PublicKey
273

274
        // BlindingPoint is the ephemeral pubkey used in route blinding.
275
        BlindingPoint *btcec.PublicKey
276

277
        // Hops is a set of blinded hops in the route, starting with the blinded
278
        // introduction node (first node id).
279
        Hops []*BlindedHop
280
}
281

282
// record produces a tlv record for a reply path.
NEW
283
func (r *ReplyPath) record() tlv.Record {
×
NEW
284
        return tlv.MakeDynamicRecord(
×
NEW
285
                replyPathType, r, r.size, encodeReplyPath, decodeReplyPath,
×
NEW
286
        )
×
NEW
287
}
×
288

289
// size returns the encoded size of our reply path.
NEW
290
func (r *ReplyPath) size() uint64 {
×
NEW
291
        // First node pubkey 33 + blinding point pubkey 33 + 1 byte for uint8
×
NEW
292
        // for our hop count.
×
NEW
293
        size := uint64(33 + 33 + 1)
×
NEW
294

×
NEW
295
        // Add each hop's size to our total.
×
NEW
296
        for _, hop := range r.Hops {
×
NEW
297
                size += hop.size()
×
NEW
298
        }
×
299

NEW
300
        return size
×
301
}
302

303
// encodeReplyPath encodes a reply path tlv.
NEW
304
func encodeReplyPath(w io.Writer, val interface{}, buf *[8]byte) error {
×
NEW
305
        if p, ok := val.(*ReplyPath); ok {
×
NEW
306
                if err := tlv.EPubKey(w, &p.FirstNodeID, buf); err != nil {
×
NEW
307
                        return fmt.Errorf("encode first node id: %w", err)
×
NEW
308
                }
×
309

NEW
310
                if err := tlv.EPubKey(w, &p.BlindingPoint, buf); err != nil {
×
NEW
311
                        return fmt.Errorf("encode blinded path: %w", err)
×
NEW
312
                }
×
313

NEW
314
                hopCount := uint8(len(p.Hops))
×
NEW
315
                if hopCount == 0 {
×
NEW
316
                        return ErrNoHops
×
NEW
317
                }
×
318

NEW
319
                if err := tlv.EUint8(w, &hopCount, buf); err != nil {
×
NEW
320
                        return fmt.Errorf("encode hop count: %w", err)
×
NEW
321
                }
×
322

NEW
323
                for i, hop := range p.Hops {
×
NEW
324
                        if err := encodeBlindedHop(w, hop, buf); err != nil {
×
NEW
325
                                return fmt.Errorf("hop %v: %w", i, err)
×
NEW
326
                        }
×
327
                }
328

NEW
329
                return nil
×
330
        }
331

NEW
332
        return tlv.NewTypeForEncodingErr(val, "*ReplyPath")
×
333
}
334

335
// decodeReplyPath decodes a reply path tlv.
336
func decodeReplyPath(r io.Reader, val interface{}, buf *[8]byte,
NEW
337
        l uint64) error {
×
NEW
338

×
NEW
339
        if p, ok := val.(*ReplyPath); ok && l > 67 {
×
NEW
340
                err := tlv.DPubKey(r, &p.FirstNodeID, buf, 33)
×
NEW
341
                if err != nil {
×
NEW
342
                        return fmt.Errorf("decode first id: %w", err)
×
NEW
343
                }
×
344

NEW
345
                err = tlv.DPubKey(r, &p.BlindingPoint, buf, 33)
×
NEW
346
                if err != nil {
×
NEW
347
                        return fmt.Errorf("decode blinding point:  %w", err)
×
NEW
348
                }
×
349

NEW
350
                var hopCount uint8
×
NEW
351
                if err := tlv.DUint8(r, &hopCount, buf, 1); err != nil {
×
NEW
352
                        return fmt.Errorf("decode hop count: %w", err)
×
NEW
353
                }
×
354

NEW
355
                if hopCount == 0 {
×
NEW
356
                        return ErrNoHops
×
NEW
357
                }
×
358

NEW
359
                for i := 0; i < int(hopCount); i++ {
×
NEW
360
                        hop := &BlindedHop{}
×
NEW
361
                        if err := decodeBlindedHop(r, hop, buf); err != nil {
×
NEW
362
                                return fmt.Errorf("decode hop: %w", err)
×
NEW
363
                        }
×
364

NEW
365
                        p.Hops = append(p.Hops, hop)
×
366
                }
367

NEW
368
                return nil
×
369
        }
370

NEW
371
        return tlv.NewTypeForDecodingErr(val, "*ReplyPath", l, l)
×
372
}
373

374
// BlindedHop contains a blinded node ID and encrypted data used to send onion
375
// messages over blinded routes.
376
type BlindedHop struct {
377
        // BlindedNodeID is the blinded node id of a node in the path.
378
        BlindedNodeID *btcec.PublicKey
379

380
        // EncryptedData is the encrypted data to be included for the node.
381
        EncryptedData []byte
382
}
383

384
// size returns the encoded size of a blinded hop.
NEW
385
func (b *BlindedHop) size() uint64 {
×
NEW
386
        // 33 byte pubkey + 2 bytes uint16 length + var bytes.
×
NEW
387
        return uint64(33 + 2 + len(b.EncryptedData))
×
NEW
388
}
×
389

390
// encodeBlindedHop encodes a blinded hop tlv.
NEW
391
func encodeBlindedHop(w io.Writer, val interface{}, buf *[8]byte) error {
×
NEW
392
        if b, ok := val.(*BlindedHop); ok {
×
NEW
393
                if err := tlv.EPubKey(w, &b.BlindedNodeID, buf); err != nil {
×
NEW
394
                        return fmt.Errorf("encode blinded id: %w", err)
×
NEW
395
                }
×
396

NEW
397
                dataLen := uint16(len(b.EncryptedData))
×
NEW
398
                if err := tlv.EUint16(w, &dataLen, buf); err != nil {
×
NEW
399
                        return fmt.Errorf("data len: %w", err)
×
NEW
400
                }
×
401

NEW
402
                if err := tlv.EVarBytes(w, &b.EncryptedData, buf); err != nil {
×
NEW
403
                        return fmt.Errorf("encode encrypted data: %w", err)
×
NEW
404
                }
×
405

NEW
406
                return nil
×
407
        }
408

NEW
409
        return tlv.NewTypeForEncodingErr(val, "*BlindedHop")
×
410
}
411

412
// decodeBlindedHop decodes a blinded hop tlv.
NEW
413
func decodeBlindedHop(r io.Reader, val interface{}, buf *[8]byte) error {
×
NEW
414
        if b, ok := val.(*BlindedHop); ok {
×
NEW
415
                err := tlv.DPubKey(r, &b.BlindedNodeID, buf, 33)
×
NEW
416
                if err != nil {
×
NEW
417
                        return fmt.Errorf("decode blinded id: %w", err)
×
NEW
418
                }
×
419

NEW
420
                var dataLen uint16
×
NEW
421
                err = tlv.DUint16(r, &dataLen, buf, 2)
×
NEW
422
                if err != nil {
×
NEW
423
                        return fmt.Errorf("decode data len: %w", err)
×
NEW
424
                }
×
425

NEW
426
                err = tlv.DVarBytes(r, &b.EncryptedData, buf, uint64(dataLen))
×
NEW
427
                if err != nil {
×
NEW
428
                        return fmt.Errorf("decode data: %w", err)
×
NEW
429
                }
×
430

NEW
431
                return nil
×
432
        }
433

NEW
434
        return tlv.NewTypeForDecodingErr(val, "*BlindedHop", 0, 0)
×
435
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc