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

lightningnetwork / lnd / 21485572389

29 Jan 2026 04:09PM UTC coverage: 65.247% (+0.2%) from 65.074%
21485572389

Pull #10089

github

web-flow
Merge 22d34d15e into 19b2ad797
Pull Request #10089: Onion message forwarding

1152 of 1448 new or added lines in 23 files covered. (79.56%)

4109 existing lines in 29 files now uncovered.

139515 of 213825 relevant lines covered (65.25%)

20529.09 hits per line

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

48.05
/lnwire/onion_msg_payload.go
1
package lnwire
2

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

10
        sphinx "github.com/lightningnetwork/lightning-onion"
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 *sphinx.BlindedPath
55

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

59
        // FinalHopTLVs contains any TLVs with type >= 64 that are reserved for
60
        // the final hop's payload.
61
        FinalHopTLVs []*FinalHopTLV
62
}
63

64
// NewOnionMessagePayload creates a new OnionMessagePayload.
65
func NewOnionMessagePayload() *OnionMessagePayload {
23✔
66
        return &OnionMessagePayload{}
23✔
67
}
23✔
68

69
// Encode encodes an onion message's payload.
70
//
71
// This is part of the lnwire.Message interface.
72
func (o *OnionMessagePayload) Encode() ([]byte, error) {
20✔
73
        var records []tlv.Record
20✔
74

20✔
75
        if o.ReplyPath != nil {
20✔
NEW
76
                records = append(records, replyPathRecord(o.ReplyPath))
×
NEW
77
        }
×
78

79
        if len(o.EncryptedData) != 0 {
40✔
80
                record := tlv.MakePrimitiveRecord(
20✔
81
                        encryptedDataTLVType, &o.EncryptedData,
20✔
82
                )
20✔
83
                records = append(records, record)
20✔
84
        }
20✔
85

86
        for _, finalHopTLV := range o.FinalHopTLVs {
21✔
87
                if err := finalHopTLV.Validate(); err != nil {
1✔
NEW
88
                        return nil, err
×
NEW
89
                }
×
90

91
                // Create a primitive record that just writes the final hop
92
                // tlv's bytes as-is. The creating function should have
93
                // encoded the value correctly.
94
                record := tlv.MakePrimitiveRecord(
1✔
95
                        finalHopTLV.TLVType, &finalHopTLV.Value,
1✔
96
                )
1✔
97
                records = append(records, record)
1✔
98
        }
99

100
        // Sort our records just in case the final hop payload records were
101
        // provided in the incorrect order.
102
        tlv.SortRecords(records)
20✔
103

20✔
104
        stream, err := tlv.NewStream(records...)
20✔
105
        if err != nil {
20✔
NEW
106
                return nil, fmt.Errorf("new stream: %w", err)
×
NEW
107
        }
×
108

109
        b := new(bytes.Buffer)
20✔
110
        if err := stream.Encode(b); err != nil {
20✔
NEW
111
                return nil, fmt.Errorf("encode stream: %w", err)
×
NEW
112
        }
×
113

114
        return b.Bytes(), nil
20✔
115
}
116

117
// Decode decodes an onion message's payload.
118
//
119
// This is part of the lnwire.Message interface.
120
func (o *OnionMessagePayload) Decode(r io.Reader) (map[tlv.Type][]byte, error) {
23✔
121
        var (
23✔
122
                invoicePayload = &FinalHopTLV{
23✔
123
                        TLVType: InvoiceNamespaceType,
23✔
124
                }
23✔
125

23✔
126
                invoiceErrorPayload = &FinalHopTLV{
23✔
127
                        TLVType: InvoiceErrorNamespaceType,
23✔
128
                }
23✔
129

23✔
130
                invoiceRequestPayload = &FinalHopTLV{
23✔
131
                        TLVType: InvoiceRequestNamespaceType,
23✔
132
                }
23✔
133
        )
23✔
134
        // Create a non-nil entry so that we can directly decode into it.
23✔
135
        o.ReplyPath = &sphinx.BlindedPath{}
23✔
136

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

23✔
164
        stream, err := tlv.NewStream(records...)
23✔
165
        if err != nil {
23✔
NEW
166
                return nil, fmt.Errorf("new stream: %w", err)
×
NEW
167
        }
×
168

169
        tlvMap, err := stream.DecodeWithParsedTypesP2P(r)
23✔
170
        if err != nil {
23✔
NEW
171
                return tlvMap, fmt.Errorf("decode stream: %w", err)
×
NEW
172
        }
×
173

174
        // If our reply path wasn't populated, replace it with a nil entry.
175
        if _, ok := tlvMap[replyPathType]; !ok {
46✔
176
                o.ReplyPath = nil
23✔
177
        }
23✔
178

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

189
                // Skip any tlvs that have been recognized in our decoding (a
190
                // zero entry means that we recognized the entry).
191
                if len(tlvBytes) == 0 {
6✔
192
                        continue
2✔
193
                }
194

195
                // Add the payload to our message's final hop payloads.
196
                payload := &FinalHopTLV{
2✔
197
                        TLVType: tlvType,
2✔
198
                        Value:   tlvBytes,
2✔
199
                }
2✔
200

2✔
201
                o.FinalHopTLVs = append(
2✔
202
                        o.FinalHopTLVs, payload,
2✔
203
                )
2✔
204
        }
205

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

216
        if _, ok := tlvMap[InvoiceErrorNamespaceType]; ok {
23✔
NEW
217
                o.FinalHopTLVs = append(
×
NEW
218
                        o.FinalHopTLVs, invoiceErrorPayload,
×
NEW
219
                )
×
NEW
220
        }
×
221

222
        if _, ok := tlvMap[InvoiceRequestNamespaceType]; ok {
25✔
223
                o.FinalHopTLVs = append(
2✔
224
                        o.FinalHopTLVs, invoiceRequestPayload,
2✔
225
                )
2✔
226
        }
2✔
227

228
        // Iteration through maps occurs in random order - sort final hop
229
        // TLVs in ascending order to make this decoding function
230
        // deterministic.
231
        sort.SliceStable(o.FinalHopTLVs, func(i, j int) bool {
23✔
NEW
232
                return o.FinalHopTLVs[i].TLVType <
×
NEW
233
                        o.FinalHopTLVs[j].TLVType
×
NEW
234
        })
×
235

236
        return tlvMap, nil
23✔
237
}
238

239
// FinalHopTLV contains values reserved for the final hop, which are just
240
// directly read from the tlv stream.
241
type FinalHopTLV struct {
242
        // TLVType is the type for the payload.
243
        TLVType tlv.Type
244

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

251
// Validate performs validation of items added to the final hop's payload in an
252
// onion. This function returns an error if a tlv is not within the range
253
// reserved for final payload.
254
func (f *FinalHopTLV) Validate() error {
1✔
255
        if f.TLVType < finalHopPayloadStart {
1✔
NEW
256
                return fmt.Errorf("%w: %v", ErrNotFinalPayload, f.TLVType)
×
NEW
257
        }
×
258

259
        return nil
1✔
260
}
261

262
// replyPathRecord produces a tlv record for a reply path.
263
func replyPathRecord(r *sphinx.BlindedPath) tlv.Record {
23✔
264
        return tlv.MakeDynamicRecord(
23✔
265
                replyPathType, r, replyPathSize(r), encodeReplyPath,
23✔
266
                decodeReplyPath,
23✔
267
        )
23✔
268
}
23✔
269

270
// replyPathSize returns the encoded size of a reply path.
271
func replyPathSize(r *sphinx.BlindedPath) func() uint64 {
23✔
272
        return func() uint64 {
23✔
NEW
273
                // First node pubkey 33 + blinding point pubkey 33 + 1 byte for
×
NEW
274
                // uint8 for our hop count.
×
NEW
275
                size := uint64(33 + 33 + 1)
×
NEW
276

×
NEW
277
                // Add each hop's size to our total.
×
NEW
278
                for _, hop := range r.BlindedHops {
×
NEW
279
                        size += blindedHopSize(hop)
×
NEW
280
                }
×
281

NEW
282
                return size
×
283
        }
284
}
285

286
// encodeReplyPath encodes a reply path tlv.
NEW
287
func encodeReplyPath(w io.Writer, val interface{}, buf *[8]byte) error {
×
NEW
288
        if p, ok := val.(*sphinx.BlindedPath); ok {
×
NEW
289
                err := tlv.EPubKey(w, &p.IntroductionPoint, buf)
×
NEW
290
                if err != nil {
×
NEW
291
                        return fmt.Errorf("encode first node id: %w", err)
×
NEW
292
                }
×
293

NEW
294
                if err := tlv.EPubKey(w, &p.BlindingPoint, buf); err != nil {
×
NEW
295
                        return fmt.Errorf("encode blinding point: %w", err)
×
NEW
296
                }
×
297

NEW
298
                hopCount := uint8(len(p.BlindedHops))
×
NEW
299
                if hopCount == 0 {
×
NEW
300
                        return ErrNoHops
×
NEW
301
                }
×
302

NEW
303
                if err := tlv.EUint8(w, &hopCount, buf); err != nil {
×
NEW
304
                        return fmt.Errorf("encode hop count: %w", err)
×
NEW
305
                }
×
306

NEW
307
                for i, hop := range p.BlindedHops {
×
NEW
308
                        if err := encodeBlindedHop(w, hop, buf); err != nil {
×
NEW
309
                                return fmt.Errorf("hop %v: %w", i, err)
×
NEW
310
                        }
×
311
                }
312

NEW
313
                return nil
×
314
        }
315

NEW
316
        return tlv.NewTypeForEncodingErr(val, "*sphinx.BlindedPath")
×
317
}
318

319
// decodeReplyPath decodes a reply path tlv.
320
func decodeReplyPath(r io.Reader, val interface{}, buf *[8]byte,
NEW
321
        l uint64) error {
×
NEW
322

×
NEW
323
        // If we have the correct type, and the length is sufficient (first node
×
NEW
324
        // pubkey (33) + blinding point (33) + hop count (1) = 67 bytes), decode
×
NEW
325
        // the reply path.
×
NEW
326
        if p, ok := val.(*sphinx.BlindedPath); ok && l > 67 {
×
NEW
327
                err := tlv.DPubKey(r, &p.IntroductionPoint, buf, 33)
×
NEW
328
                if err != nil {
×
NEW
329
                        return fmt.Errorf("decode first id: %w", err)
×
NEW
330
                }
×
331

NEW
332
                err = tlv.DPubKey(r, &p.BlindingPoint, buf, 33)
×
NEW
333
                if err != nil {
×
NEW
334
                        return fmt.Errorf("decode blinding point:  %w", err)
×
NEW
335
                }
×
336

NEW
337
                var hopCount uint8
×
NEW
338
                if err := tlv.DUint8(r, &hopCount, buf, 1); err != nil {
×
NEW
339
                        return fmt.Errorf("decode hop count: %w", err)
×
NEW
340
                }
×
341

NEW
342
                if hopCount == 0 {
×
NEW
343
                        return ErrNoHops
×
NEW
344
                }
×
345

NEW
346
                for i := 0; i < int(hopCount); i++ {
×
NEW
347
                        hop := &sphinx.BlindedHopInfo{}
×
NEW
348
                        if err := decodeBlindedHop(r, hop, buf); err != nil {
×
NEW
349
                                return fmt.Errorf("decode hop: %w", err)
×
NEW
350
                        }
×
351

NEW
352
                        p.BlindedHops = append(p.BlindedHops, hop)
×
353
                }
354

NEW
355
                return nil
×
356
        }
357

NEW
358
        return tlv.NewTypeForDecodingErr(val, "*sphinx.BlindedPath", l, l)
×
359
}
360

361
// blindedHopSize returns the encoded size of a blinded hop.
NEW
362
func blindedHopSize(b *sphinx.BlindedHopInfo) uint64 {
×
NEW
363
        // 33 byte pubkey + 2 bytes uint16 length + var bytes.
×
NEW
364
        return uint64(33 + 2 + len(b.CipherText))
×
NEW
365
}
×
366

367
// encodeBlindedHop encodes a blinded hop tlv.
NEW
368
func encodeBlindedHop(w io.Writer, val interface{}, buf *[8]byte) error {
×
NEW
369
        if b, ok := val.(*sphinx.BlindedHopInfo); ok {
×
NEW
370
                if err := tlv.EPubKey(w, &b.BlindedNodePub, buf); err != nil {
×
NEW
371
                        return fmt.Errorf("encode blinded id: %w", err)
×
NEW
372
                }
×
373

NEW
374
                dataLen := uint16(len(b.CipherText))
×
NEW
375
                if err := tlv.EUint16(w, &dataLen, buf); err != nil {
×
NEW
376
                        return fmt.Errorf("data len: %w", err)
×
NEW
377
                }
×
378

NEW
379
                if err := tlv.EVarBytes(w, &b.CipherText, buf); err != nil {
×
NEW
380
                        return fmt.Errorf("encode encrypted data: %w", err)
×
NEW
381
                }
×
382

NEW
383
                return nil
×
384
        }
385

NEW
386
        return tlv.NewTypeForEncodingErr(val, "*sphinx.BlindedHopInfo")
×
387
}
388

389
// decodeBlindedHop decodes a blinded hop tlv.
NEW
390
func decodeBlindedHop(r io.Reader, val interface{}, buf *[8]byte) error {
×
NEW
391
        if b, ok := val.(*sphinx.BlindedHopInfo); ok {
×
NEW
392
                err := tlv.DPubKey(r, &b.BlindedNodePub, buf, 33)
×
NEW
393
                if err != nil {
×
NEW
394
                        return fmt.Errorf("decode blinded id: %w", err)
×
NEW
395
                }
×
396

NEW
397
                var dataLen uint16
×
NEW
398
                err = tlv.DUint16(r, &dataLen, buf, 2)
×
NEW
399
                if err != nil {
×
NEW
400
                        return fmt.Errorf("decode data len: %w", err)
×
NEW
401
                }
×
402

NEW
403
                err = tlv.DVarBytes(r, &b.CipherText, buf, uint64(dataLen))
×
NEW
404
                if err != nil {
×
NEW
405
                        return fmt.Errorf("decode data: %w", err)
×
NEW
406
                }
×
407

NEW
408
                return nil
×
409
        }
410

NEW
411
        return tlv.NewTypeForDecodingErr(val, "*sphinx.BlindedHopInfo", 0, 0)
×
412
}
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

© 2026 Coveralls, Inc