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

lightningnetwork / lnd / 16673052227

01 Aug 2025 10:44AM UTC coverage: 67.016% (-0.03%) from 67.047%
16673052227

Pull #9888

github

web-flow
Merge 1dd8765d7 into 37523b6cb
Pull Request #9888: Attributable failures

325 of 384 new or added lines in 16 files covered. (84.64%)

131 existing lines in 24 files now uncovered.

135611 of 202355 relevant lines covered (67.02%)

21613.83 hits per line

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

78.0
/htlcswitch/hop/iterator.go
1
package hop
2

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

10
        "github.com/btcsuite/btcd/btcec/v2"
11
        "github.com/btcsuite/btcd/chaincfg/chainhash"
12
        sphinx "github.com/lightningnetwork/lightning-onion"
13
        "github.com/lightningnetwork/lnd/lnwire"
14
        "github.com/lightningnetwork/lnd/record"
15
        "github.com/lightningnetwork/lnd/tlv"
16
)
17

18
var (
19
        // ErrDecodeFailed is returned when we can't decode blinded data.
20
        ErrDecodeFailed = errors.New("could not decode blinded data")
21

22
        // ErrNoBlindingPoint is returned when we have not provided a blinding
23
        // point for a validated payload with encrypted data set.
24
        ErrNoBlindingPoint = errors.New("no blinding point set for validated " +
25
                "blinded hop")
26
)
27

28
// RouteRole represents the different types of roles a node can have as a
29
// recipient of a HTLC.
30
type RouteRole uint8
31

32
const (
33
        // RouteRoleCleartext represents a regular route hop.
34
        RouteRoleCleartext RouteRole = iota
35

36
        // RouteRoleIntroduction represents an introduction node in a blinded
37
        // path, characterized by a blinding point in the onion payload.
38
        RouteRoleIntroduction
39

40
        // RouteRoleRelaying represents a relaying node in a blinded path,
41
        // characterized by a blinding point in update_add_htlc.
42
        RouteRoleRelaying
43
)
44

45
// String representation of a role in a route.
46
func (h RouteRole) String() string {
×
47
        switch h {
×
48
        case RouteRoleCleartext:
×
49
                return "cleartext"
×
50

51
        case RouteRoleRelaying:
×
52
                return "blinded relay"
×
53

54
        case RouteRoleIntroduction:
×
55
                return "introduction node"
×
56

57
        default:
×
58
                return fmt.Sprintf("unknown route role: %d", h)
×
59
        }
60
}
61

62
// NewRouteRole returns the role we're playing in a route depending on the
63
// blinding points set (or not). If we are in the situation where we received
64
// blinding points in both the update add message and the payload:
65
//   - We must have had a valid update add blinding point, because we were able
66
//     to decrypt our onion to get the payload blinding point.
67
//   - We return a relaying node role, because an introduction node (by
68
//     definition) does not receive a blinding point in update add.
69
//   - We assume the sending node to be buggy (including a payload blinding
70
//     where it shouldn't), and rely on validation elsewhere to handle this.
71
func NewRouteRole(updateAddBlinding, payloadBlinding bool) RouteRole {
4✔
72
        switch {
4✔
73
        case updateAddBlinding:
3✔
74
                return RouteRoleRelaying
3✔
75

76
        case payloadBlinding:
3✔
77
                return RouteRoleIntroduction
3✔
78

79
        default:
4✔
80
                return RouteRoleCleartext
4✔
81
        }
82
}
83

84
// Iterator is an interface that abstracts away the routing information
85
// included in HTLC's which includes the entirety of the payment path of an
86
// HTLC. This interface provides two basic method which carry out: how to
87
// interpret the forwarding information encoded within the HTLC packet, and hop
88
// to encode the forwarding information for the _next_ hop.
89
type Iterator interface {
90
        // HopPayload returns the set of fields that detail exactly _how_ this
91
        // hop should forward the HTLC to the next hop.  Additionally, the
92
        // information encoded within the returned ForwardingInfo is to be used
93
        // by each hop to authenticate the information given to it by the prior
94
        // hop. The payload will also contain any additional TLV fields provided
95
        // by the sender. The role that this hop plays in the context of
96
        // route blinding (regular, introduction or relaying) is returned
97
        // whenever the payload is successfully parsed, even if we subsequently
98
        // face a validation error.
99
        HopPayload() (*Payload, RouteRole, error)
100

101
        // EncodeNextHop encodes the onion packet destined for the next hop
102
        // into the passed io.Writer.
103
        EncodeNextHop(w io.Writer) error
104

105
        // ExtractEncrypterParams extracts the ephemeral key and shared secret
106
        // from the onion packet and returns them to the caller along with a
107
        // failure code to signal if the decoding was successful.
108
        ExtractEncrypterParams(SharedSecretGenerator) (*btcec.PublicKey,
109
                sphinx.Hash256, lnwire.BlindingPointRecord, lnwire.FailCode)
110
}
111

112
// sphinxHopIterator is the Sphinx implementation of hop iterator which uses
113
// onion routing to encode the payment route  in such a way so that node might
114
// see only the next hop in the route.
115
type sphinxHopIterator struct {
116
        // ogPacket is the original packet from which the processed packet is
117
        // derived.
118
        ogPacket *sphinx.OnionPacket
119

120
        // processedPacket is the outcome of processing an onion packet. It
121
        // includes the information required to properly forward the packet to
122
        // the next hop.
123
        processedPacket *sphinx.ProcessedPacket
124

125
        // blindingKit contains the elements required to process hops that are
126
        // part of a blinded route.
127
        blindingKit BlindingKit
128

129
        // rHash holds the payment hash for this payment. This is needed for
130
        // when a new hop iterator is constructed.
131
        rHash []byte
132

133
        // router holds the router which can be used to decrypt onion payloads.
134
        // This is required for peeling of dummy hops in a blinded path where
135
        // the same node will iteratively need to unwrap the onion.
136
        router *sphinx.Router
137
}
138

139
// makeSphinxHopIterator converts a processed packet returned from a sphinx
140
// router and converts it into an hop iterator for usage in the link. A
141
// blinding kit is passed through for the link to obtain forwarding information
142
// for blinded routes.
143
func makeSphinxHopIterator(router *sphinx.Router, ogPacket *sphinx.OnionPacket,
144
        packet *sphinx.ProcessedPacket, blindingKit BlindingKit,
145
        rHash []byte) *sphinxHopIterator {
3✔
146

3✔
147
        return &sphinxHopIterator{
3✔
148
                router:          router,
3✔
149
                ogPacket:        ogPacket,
3✔
150
                processedPacket: packet,
3✔
151
                blindingKit:     blindingKit,
3✔
152
                rHash:           rHash,
3✔
153
        }
3✔
154
}
3✔
155

156
// A compile time check to ensure sphinxHopIterator implements the HopIterator
157
// interface.
158
var _ Iterator = (*sphinxHopIterator)(nil)
159

160
// Encode encodes iterator and writes it to the writer.
161
//
162
// NOTE: Part of the HopIterator interface.
163
func (r *sphinxHopIterator) EncodeNextHop(w io.Writer) error {
3✔
164
        return r.processedPacket.NextPacket.Encode(w)
3✔
165
}
3✔
166

167
// HopPayload returns the set of fields that detail exactly _how_ this hop
168
// should forward the HTLC to the next hop.  Additionally, the information
169
// encoded within the returned ForwardingInfo is to be used by each hop to
170
// authenticate the information given to it by the prior hop. The role that
171
// this hop plays in the context of route blinding (regular, introduction or
172
// relaying) is returned whenever the payload is successfully parsed, even if
173
// we subsequently face a validation error. The payload will also contain any
174
// additional TLV fields provided by the sender.
175
//
176
// NOTE: Part of the HopIterator interface.
177
func (r *sphinxHopIterator) HopPayload() (*Payload, RouteRole, error) {
5✔
178
        switch r.processedPacket.Payload.Type {
5✔
179

180
        // If this is the legacy payload, then we'll extract the information
181
        // directly from the pre-populated ForwardingInstructions field.
182
        case sphinx.PayloadLegacy:
1✔
183
                fwdInst := r.processedPacket.ForwardingInstructions
1✔
184
                return NewLegacyPayload(fwdInst), RouteRoleCleartext, nil
1✔
185

186
        // Otherwise, if this is the TLV payload, then we'll make a new stream
187
        // to decode only what we need to make routing decisions.
188
        case sphinx.PayloadTLV:
4✔
189
                return extractTLVPayload(r)
4✔
190

191
        default:
×
192
                return nil, RouteRoleCleartext,
×
193
                        fmt.Errorf("unknown sphinx payload type: %v",
×
194
                                r.processedPacket.Payload.Type)
×
195
        }
196
}
197

198
// extractTLVPayload parses the hop payload and assumes that it uses the TLV
199
// format. It returns the parsed payload along with the RouteRole that this hop
200
// plays given the contents of the payload.
201
func extractTLVPayload(r *sphinxHopIterator) (*Payload, RouteRole, error) {
4✔
202
        isFinal := r.processedPacket.Action == sphinx.ExitNode
4✔
203

4✔
204
        // Initial payload parsing and validation
4✔
205
        payload, routeRole, recipientData, err := parseAndValidateSenderPayload(
4✔
206
                r.processedPacket.Payload.Payload, isFinal,
4✔
207
                r.blindingKit.UpdateAddBlinding.IsSome(),
4✔
208
        )
4✔
209
        if err != nil {
4✔
210
                return nil, routeRole, err
×
211
        }
×
212

213
        // If the payload contained no recipient data, then we can exit now.
214
        if !recipientData {
8✔
215
                return payload, routeRole, nil
4✔
216
        }
4✔
217

218
        return parseAndValidateRecipientData(r, payload, isFinal, routeRole)
3✔
219
}
220

221
// parseAndValidateRecipientData decrypts the payload from the recipient and
222
// then continues handling and validation based on if we are a forwarding node
223
// in this blinded path or the final destination node.
224
func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload,
225
        isFinal bool, routeRole RouteRole) (*Payload, RouteRole, error) {
9✔
226

9✔
227
        // Decrypt and validate the blinded route data
9✔
228
        routeData, blindingPoint, err := decryptAndValidateBlindedRouteData(
9✔
229
                r, payload,
9✔
230
        )
9✔
231
        if err != nil {
16✔
232
                return nil, routeRole, err
7✔
233
        }
7✔
234

235
        // This is the final node in the blinded route.
236
        if isFinal {
8✔
237
                return deriveBlindedRouteFinalHopForwardingInfo(
3✔
238
                        routeData, payload, routeRole,
3✔
239
                )
3✔
240
        }
3✔
241

242
        // Else, we are a forwarding node in this blinded path.
243
        return deriveBlindedRouteForwardingInfo(
5✔
244
                r, routeData, payload, routeRole, blindingPoint,
5✔
245
        )
5✔
246
}
247

248
// deriveBlindedRouteFinalHopForwardingInfo extracts the PathID from the
249
// routeData and constructs the ForwardingInfo accordingly.
250
func deriveBlindedRouteFinalHopForwardingInfo(
251
        routeData *record.BlindedRouteData, payload *Payload,
252
        routeRole RouteRole) (*Payload, RouteRole, error) {
3✔
253

3✔
254
        var pathID *chainhash.Hash
3✔
255
        routeData.PathID.WhenSome(func(r tlv.RecordT[tlv.TlvType6, []byte]) {
6✔
256
                var id chainhash.Hash
3✔
257
                copy(id[:], r.Val)
3✔
258
                pathID = &id
3✔
259
        })
3✔
260
        if pathID == nil {
3✔
261
                return nil, routeRole, ErrInvalidPayload{
×
262
                        Type:      tlv.Type(6),
×
263
                        Violation: InsufficientViolation,
×
264
                }
×
265
        }
×
266

267
        payload.FwdInfo = ForwardingInfo{
3✔
268
                PathID: pathID,
3✔
269
        }
3✔
270

3✔
271
        return payload, routeRole, nil
3✔
272
}
273

274
// deriveBlindedRouteForwardingInfo uses the parsed BlindedRouteData from the
275
// recipient to derive the ForwardingInfo for the payment.
276
func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator,
277
        routeData *record.BlindedRouteData, payload *Payload,
278
        routeRole RouteRole, blindingPoint *btcec.PublicKey) (*Payload,
279
        RouteRole, error) {
5✔
280

5✔
281
        relayInfo, err := routeData.RelayInfo.UnwrapOrErr(
5✔
282
                fmt.Errorf("relay info not set for non-final blinded hop"),
5✔
283
        )
5✔
284
        if err != nil {
5✔
285
                return nil, routeRole, err
×
286
        }
×
287

288
        fwdAmt, err := calculateForwardingAmount(
5✔
289
                r.blindingKit.IncomingAmount, relayInfo.Val.BaseFee,
5✔
290
                relayInfo.Val.FeeRate,
5✔
291
        )
5✔
292
        if err != nil {
5✔
293
                return nil, routeRole, err
×
294
        }
×
295

296
        nextEph, err := routeData.NextBlindingOverride.UnwrapOrFuncErr(
5✔
297
                func() (tlv.RecordT[tlv.TlvType8, *btcec.PublicKey], error) {
10✔
298
                        next, err := r.blindingKit.Processor.NextEphemeral(
5✔
299
                                blindingPoint,
5✔
300
                        )
5✔
301
                        if err != nil {
5✔
302
                                return routeData.NextBlindingOverride.Zero(),
×
303
                                        err
×
304
                        }
×
305

306
                        return tlv.NewPrimitiveRecord[tlv.TlvType8](next), nil
5✔
307
                })
308
        if err != nil {
5✔
309
                return nil, routeRole, err
×
310
        }
×
311

312
        // If the payload signals that the following hop is a dummy hop, then
313
        // we will iteratively peel the dummy hop until we reach the final
314
        // payload.
315
        if checkForDummyHop(routeData, r.router.OnionPublicKey()) {
8✔
316
                return peelBlindedPathDummyHop(
3✔
317
                        r, uint32(relayInfo.Val.CltvExpiryDelta), fwdAmt,
3✔
318
                        routeRole, nextEph,
3✔
319
                )
3✔
320
        }
3✔
321

322
        nextSCID, err := routeData.ShortChannelID.UnwrapOrErr(
5✔
323
                fmt.Errorf("next SCID not set for non-final blinded hop"),
5✔
324
        )
5✔
325
        if err != nil {
5✔
326
                return nil, routeRole, err
×
327
        }
×
328
        payload.FwdInfo = ForwardingInfo{
5✔
329
                NextHop:         nextSCID.Val,
5✔
330
                AmountToForward: fwdAmt,
5✔
331
                OutgoingCTLV: r.blindingKit.IncomingCltv - uint32(
5✔
332
                        relayInfo.Val.CltvExpiryDelta,
5✔
333
                ),
5✔
334
                // Remap from blinding override type to blinding point type.
5✔
335
                NextBlinding: tlv.SomeRecordT(
5✔
336
                        tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
5✔
337
                                nextEph.Val,
5✔
338
                        ),
5✔
339
                ),
5✔
340
        }
5✔
341

5✔
342
        return payload, routeRole, nil
5✔
343
}
344

345
// checkForDummyHop returns whether the given BlindedRouteData packet indicates
346
// the presence of a dummy hop.
347
func checkForDummyHop(routeData *record.BlindedRouteData,
348
        routerPubKey *btcec.PublicKey) bool {
5✔
349

5✔
350
        var isDummy bool
5✔
351
        routeData.NextNodeID.WhenSome(
5✔
352
                func(r tlv.RecordT[tlv.TlvType4, *btcec.PublicKey]) {
8✔
353
                        isDummy = r.Val.IsEqual(routerPubKey)
3✔
354
                },
3✔
355
        )
356

357
        return isDummy
5✔
358
}
359

360
// peelBlindedPathDummyHop packages the next onion packet and then constructs
361
// a new hop iterator using our router and then proceeds to process the next
362
// packet. This can only be done for blinded route dummy hops since we expect
363
// to be the final hop on the path.
364
func peelBlindedPathDummyHop(r *sphinxHopIterator, cltvExpiryDelta uint32,
365
        fwdAmt lnwire.MilliSatoshi, routeRole RouteRole,
366
        nextEph tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) (*Payload,
367
        RouteRole, error) {
3✔
368

3✔
369
        onionPkt := r.processedPacket.NextPacket
3✔
370
        sphinxPacket, err := r.router.ReconstructOnionPacket(
3✔
371
                onionPkt, r.rHash, sphinx.WithBlindingPoint(nextEph.Val),
3✔
372
        )
3✔
373
        if err != nil {
3✔
374
                return nil, routeRole, err
×
375
        }
×
376

377
        iterator := makeSphinxHopIterator(
3✔
378
                r.router, onionPkt, sphinxPacket, BlindingKit{
3✔
379
                        Processor: r.router,
3✔
380
                        UpdateAddBlinding: tlv.SomeRecordT(
3✔
381
                                tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( //nolint:ll
3✔
382
                                        nextEph.Val,
3✔
383
                                ),
3✔
384
                        ),
3✔
385
                        IncomingAmount: fwdAmt,
3✔
386
                        IncomingCltv: r.blindingKit.IncomingCltv -
3✔
387
                                cltvExpiryDelta,
3✔
388
                }, r.rHash,
3✔
389
        )
3✔
390

3✔
391
        return extractTLVPayload(iterator)
3✔
392
}
393

394
// decryptAndValidateBlindedRouteData decrypts the encrypted payload from the
395
// payment recipient using a blinding key. The incoming HTLC amount and CLTV
396
// values are then verified against the policy values from the recipient.
397
func decryptAndValidateBlindedRouteData(r *sphinxHopIterator,
398
        payload *Payload) (*record.BlindedRouteData, *btcec.PublicKey, error) {
9✔
399

9✔
400
        blindingPoint, err := r.blindingKit.getBlindingPoint(
9✔
401
                payload.blindingPoint,
9✔
402
        )
9✔
403
        if err != nil {
10✔
404
                return nil, nil, err
1✔
405
        }
1✔
406

407
        decrypted, err := r.blindingKit.Processor.DecryptBlindedHopData(
8✔
408
                blindingPoint, payload.encryptedData,
8✔
409
        )
8✔
410
        if err != nil {
12✔
411
                return nil, nil, fmt.Errorf("decrypt blinded data: %w", err)
4✔
412
        }
4✔
413

414
        buf := bytes.NewBuffer(decrypted)
7✔
415
        routeData, err := record.DecodeBlindedRouteData(buf)
7✔
416
        if err != nil {
8✔
417
                return nil, nil, fmt.Errorf("%w: %w", ErrDecodeFailed, err)
1✔
418
        }
1✔
419

420
        err = ValidateBlindedRouteData(
6✔
421
                routeData, r.blindingKit.IncomingAmount,
6✔
422
                r.blindingKit.IncomingCltv,
6✔
423
        )
6✔
424
        if err != nil {
7✔
425
                return nil, nil, err
1✔
426
        }
1✔
427

428
        return routeData, blindingPoint, nil
5✔
429
}
430

431
// parseAndValidateSenderPayload parses the payload bytes received from the
432
// onion constructor (the sender) and validates that various fields have been
433
// set. It also uses the presence of a blinding key in either the
434
// update_add_htlc message or in the payload to determine the RouteRole.
435
// The RouteRole is returned even if an error is returned. The boolean return
436
// value indicates that the sender payload includes encrypted data from the
437
// recipient that should be parsed.
438
func parseAndValidateSenderPayload(payloadBytes []byte, isFinalHop,
439
        updateAddBlindingSet bool) (*Payload, RouteRole, bool, error) {
4✔
440

4✔
441
        // Extract TLVs from the packet constructor (the sender).
4✔
442
        payload, parsed, err := ParseTLVPayload(bytes.NewReader(payloadBytes))
4✔
443
        if err != nil {
4✔
444
                // If we couldn't even parse our payload then we do a
×
445
                // best-effort of determining our role in a blinded route,
×
446
                // accepting that we can't know whether we were the introduction
×
447
                // node (as the payload is not parseable).
×
448
                routeRole := RouteRoleCleartext
×
449
                if updateAddBlindingSet {
×
450
                        routeRole = RouteRoleRelaying
×
451
                }
×
452

453
                return nil, routeRole, false, err
×
454
        }
455

456
        // Now that we've parsed our payload we can determine which role we're
457
        // playing in the route.
458
        _, payloadBlinding := parsed[record.BlindingPointOnionType]
4✔
459
        routeRole := NewRouteRole(updateAddBlindingSet, payloadBlinding)
4✔
460

4✔
461
        // Validate the presence of the various payload fields we received from
4✔
462
        // the sender.
4✔
463
        err = ValidateTLVPayload(parsed, isFinalHop, updateAddBlindingSet)
4✔
464
        if err != nil {
4✔
465
                return nil, routeRole, false, err
×
466
        }
×
467

468
        // If there is no encrypted data from the receiver then return the
469
        // payload as is since the forwarding info would have been received
470
        // from the sender.
471
        if payload.encryptedData == nil {
8✔
472
                return payload, routeRole, false, nil
4✔
473
        }
4✔
474

475
        // Validate the presence of various fields in the sender payload given
476
        // that we now know that this is a hop with instructions from the
477
        // recipient.
478
        err = ValidatePayloadWithBlinded(isFinalHop, parsed)
3✔
479
        if err != nil {
3✔
480
                return payload, routeRole, true, err
×
481
        }
×
482

483
        return payload, routeRole, true, nil
3✔
484
}
485

486
// ExtractEncrypterParams extracts the ephemeral key, shared secret and blinding
487
// point record from the onion packet and returns them to the caller along with
488
// a failure code to signal if the decoding was successful.
489
//
490
// NOTE: Part of the HopIterator interface.
491
func (r *sphinxHopIterator) ExtractEncrypterParams(
492
        extracter SharedSecretGenerator) (*btcec.PublicKey, sphinx.Hash256,
493
        lnwire.BlindingPointRecord, lnwire.FailCode) {
3✔
494

3✔
495
        sharedSecret, failCode := extracter(r.ogPacket.EphemeralKey)
3✔
496
        if failCode != lnwire.CodeNone {
3✔
NEW
497
                return nil, sphinx.Hash256{}, r.blindingKit.UpdateAddBlinding,
×
NEW
498
                        failCode
×
UNCOV
499
        }
×
500

501
        return r.ogPacket.EphemeralKey, sharedSecret,
3✔
502
                r.blindingKit.UpdateAddBlinding, lnwire.CodeNone
3✔
503
}
504

505
// BlindingProcessor is an interface that provides the cryptographic operations
506
// required for processing blinded hops.
507
//
508
// This interface is extracted to allow more granular testing of blinded
509
// forwarding calculations.
510
type BlindingProcessor interface {
511
        // DecryptBlindedHopData decrypts a blinded blob of data using the
512
        // ephemeral key provided.
513
        DecryptBlindedHopData(ephemPub *btcec.PublicKey,
514
                encryptedData []byte) ([]byte, error)
515

516
        // NextEphemeral returns the next hop's ephemeral key, calculated
517
        // from the current ephemeral key provided.
518
        NextEphemeral(*btcec.PublicKey) (*btcec.PublicKey, error)
519
}
520

521
// BlindingKit contains the components required to extract forwarding
522
// information for hops in a blinded route.
523
type BlindingKit struct {
524
        // Processor provides the low-level cryptographic operations to
525
        // handle an encrypted blob of data in a blinded forward.
526
        Processor BlindingProcessor
527

528
        // UpdateAddBlinding holds a blinding point that was passed to the
529
        // node via update_add_htlc's TLVs.
530
        UpdateAddBlinding lnwire.BlindingPointRecord
531

532
        // IncomingCltv is the expiry of the incoming HTLC.
533
        IncomingCltv uint32
534

535
        // IncomingAmount is the amount of the incoming HTLC.
536
        IncomingAmount lnwire.MilliSatoshi
537
}
538

539
// getBlindingPoint returns either the payload or updateAddHtlc blinding point,
540
// assuming that validation that these values are appropriately set has already
541
// been handled elsewhere.
542
func (b *BlindingKit) getBlindingPoint(payloadBlinding *btcec.PublicKey) (
543
        *btcec.PublicKey, error) {
9✔
544

9✔
545
        payloadBlindingSet := payloadBlinding != nil
9✔
546
        updateBlindingSet := b.UpdateAddBlinding.IsSome()
9✔
547

9✔
548
        switch {
9✔
549
        case payloadBlindingSet:
4✔
550
                return payloadBlinding, nil
4✔
551

552
        case updateBlindingSet:
7✔
553
                pk, err := b.UpdateAddBlinding.UnwrapOrErr(
7✔
554
                        fmt.Errorf("expected update add blinding"),
7✔
555
                )
7✔
556
                if err != nil {
7✔
557
                        return nil, err
×
558
                }
×
559

560
                return pk.Val, nil
7✔
561

562
        default:
1✔
563
                return nil, ErrNoBlindingPoint
1✔
564
        }
565
}
566

567
// calculateForwardingAmount calculates the amount to forward for a blinded
568
// hop based on the incoming amount and forwarding parameters.
569
//
570
// When forwarding a payment, the fee we take is calculated, not on the
571
// incoming amount, but rather on the amount we forward. We charge fees based
572
// on our own liquidity we are forwarding downstream.
573
//
574
// With route blinding, we are NOT given the amount to forward.  This
575
// unintuitive looking formula comes from the fact that without the amount to
576
// forward, we cannot compute the fees taken directly.
577
//
578
// The amount to be forwarded can be computed as follows:
579
//
580
// amt_to_forward = incoming_amount - total_fees
581
// total_fees = base_fee + amt_to_forward*(fee_rate/1000000)
582
//
583
// Solving for amount_to_forward:
584
// amt_to_forward = incoming_amount - base_fee - (amount_to_forward * fee_rate)/1e6
585
// amt_to_forward + (amount_to_forward * fee_rate) / 1e6 = incoming_amount - base_fee
586
// amt_to_forward * 1e6 + (amount_to_forward * fee_rate) = (incoming_amount - base_fee) * 1e6
587
// amt_to_forward * (1e6 + fee_rate) = (incoming_amount - base_fee) * 1e6
588
// amt_to_forward = ((incoming_amount - base_fee) * 1e6) / (1e6 + fee_rate)
589
//
590
// From there we use a ceiling formula for integer division so that we always
591
// round up, otherwise the sender may receive slightly less than intended:
592
//
593
// ceil(a/b) = (a + b - 1)/(b).
594
//
595
//nolint:ll,dupword
596
func calculateForwardingAmount(incomingAmount, baseFee lnwire.MilliSatoshi,
597
        proportionalFee uint32) (lnwire.MilliSatoshi, error) {
8✔
598

8✔
599
        // Sanity check to prevent overflow.
8✔
600
        if incomingAmount < baseFee {
9✔
601
                return 0, fmt.Errorf("incoming amount: %v < base fee: %v",
1✔
602
                        incomingAmount, baseFee)
1✔
603
        }
1✔
604
        numerator := (uint64(incomingAmount) - uint64(baseFee)) * 1e6
7✔
605
        denominator := 1e6 + uint64(proportionalFee)
7✔
606

7✔
607
        ceiling := (numerator + denominator - 1) / denominator
7✔
608

7✔
609
        return lnwire.MilliSatoshi(ceiling), nil
7✔
610
}
611

612
// OnionProcessor is responsible for keeping all sphinx dependent parts inside
613
// and expose only decoding function. With such approach we give freedom for
614
// subsystems which wants to decode sphinx path to not be dependable from
615
// sphinx at all.
616
//
617
// NOTE: The reason for keeping decoder separated from hop iterator is too
618
// maintain the hop iterator abstraction. Without it the structures which using
619
// the hop iterator should contain sphinx router which makes their creations in
620
// tests dependent from the sphinx internal parts.
621
type OnionProcessor struct {
622
        router *sphinx.Router
623
}
624

625
// NewOnionProcessor creates new instance of decoder.
626
func NewOnionProcessor(router *sphinx.Router) *OnionProcessor {
23✔
627
        return &OnionProcessor{router}
23✔
628
}
23✔
629

630
// Start spins up the onion processor's sphinx router.
631
func (p *OnionProcessor) Start() error {
3✔
632
        log.Info("Onion processor starting")
3✔
633
        return p.router.Start()
3✔
634
}
3✔
635

636
// Stop shutsdown the onion processor's sphinx router.
637
func (p *OnionProcessor) Stop() error {
4✔
638

4✔
639
        log.Info("Onion processor shutting down...")
4✔
640
        defer log.Debug("Onion processor shutdown complete")
4✔
641

4✔
642
        p.router.Stop()
4✔
643
        return nil
4✔
644
}
4✔
645

646
// ReconstructBlindingInfo contains the information required to reconstruct a
647
// blinded onion.
648
type ReconstructBlindingInfo struct {
649
        // BlindingKey is the blinding point set in UpdateAddHTLC.
650
        BlindingKey lnwire.BlindingPointRecord
651

652
        // IncomingAmt is the amount for the incoming HTLC.
653
        IncomingAmt lnwire.MilliSatoshi
654

655
        // IncomingExpiry is the expiry height of the incoming HTLC.
656
        IncomingExpiry uint32
657
}
658

659
// ReconstructHopIterator attempts to decode a valid sphinx packet from the
660
// passed io.Reader instance using the rHash as the associated data when
661
// checking the relevant MACs during the decoding process.
662
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
663
        blindingInfo ReconstructBlindingInfo) (Iterator, error) {
3✔
664

3✔
665
        onionPkt := &sphinx.OnionPacket{}
3✔
666
        if err := onionPkt.Decode(r); err != nil {
3✔
667
                return nil, err
×
668
        }
×
669

670
        var opts []sphinx.ProcessOnionOpt
3✔
671
        blindingInfo.BlindingKey.WhenSome(func(
3✔
672
                r tlv.RecordT[lnwire.BlindingPointTlvType, *btcec.PublicKey]) {
6✔
673

3✔
674
                opts = append(opts, sphinx.WithBlindingPoint(r.Val))
3✔
675
        })
3✔
676

677
        // Attempt to process the Sphinx packet. We include the payment hash of
678
        // the HTLC as it's authenticated within the Sphinx packet itself as
679
        // associated data in order to thwart attempts a replay attacks. In the
680
        // case of a replay, an attacker is *forced* to use the same payment
681
        // hash twice, thereby losing their money entirely.
682
        sphinxPacket, err := p.router.ReconstructOnionPacket(
3✔
683
                onionPkt, rHash, opts...,
3✔
684
        )
3✔
685
        if err != nil {
3✔
686
                return nil, err
×
687
        }
×
688

689
        return makeSphinxHopIterator(p.router, onionPkt, sphinxPacket,
3✔
690
                BlindingKit{
3✔
691
                        Processor:         p.router,
3✔
692
                        UpdateAddBlinding: blindingInfo.BlindingKey,
3✔
693
                        IncomingAmount:    blindingInfo.IncomingAmt,
3✔
694
                        IncomingCltv:      blindingInfo.IncomingExpiry,
3✔
695
                }, rHash,
3✔
696
        ), nil
3✔
697
}
698

699
// DecodeHopIteratorRequest encapsulates all date necessary to process an onion
700
// packet, perform sphinx replay detection, and schedule the entry for garbage
701
// collection.
702
type DecodeHopIteratorRequest struct {
703
        OnionReader    io.Reader
704
        RHash          []byte
705
        IncomingCltv   uint32
706
        IncomingAmount lnwire.MilliSatoshi
707
        BlindingPoint  lnwire.BlindingPointRecord
708
}
709

710
// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
711
// processing.
712
type DecodeHopIteratorResponse struct {
713
        HopIterator Iterator
714
        FailCode    lnwire.FailCode
715
}
716

717
// Result returns the (HopIterator, lnwire.FailCode) tuple, which should
718
// correspond to the index of a particular DecodeHopIteratorRequest.
719
//
720
// NOTE: The HopIterator should be considered invalid if the fail code is
721
// anything but lnwire.CodeNone.
722
func (r *DecodeHopIteratorResponse) Result() (Iterator, lnwire.FailCode) {
436✔
723
        return r.HopIterator, r.FailCode
436✔
724
}
436✔
725

726
// DecodeHopIterators performs batched decoding and validation of incoming
727
// sphinx packets. For the same `id`, this method will return the same iterators
728
// and failcodes upon subsequent invocations.
729
//
730
// NOTE: In order for the responses to be valid, the caller must guarantee that
731
// the presented readers and rhashes *NEVER* deviate across invocations for the
732
// same id.
733
func (p *OnionProcessor) DecodeHopIterators(id []byte,
734
        reqs []DecodeHopIteratorRequest,
735
        reforward bool) ([]DecodeHopIteratorResponse, error) {
3✔
736

3✔
737
        var (
3✔
738
                batchSize = len(reqs)
3✔
739
                onionPkts = make([]sphinx.OnionPacket, batchSize)
3✔
740
                resps     = make([]DecodeHopIteratorResponse, batchSize)
3✔
741
        )
3✔
742

3✔
743
        tx := p.router.BeginTxn(id, batchSize)
3✔
744

3✔
745
        decode := func(seqNum uint16, onionPkt *sphinx.OnionPacket,
3✔
746
                req DecodeHopIteratorRequest) lnwire.FailCode {
6✔
747

3✔
748
                err := onionPkt.Decode(req.OnionReader)
3✔
749
                switch err {
3✔
750
                case nil:
3✔
751
                        // success
752

753
                case sphinx.ErrInvalidOnionVersion:
×
754
                        return lnwire.CodeInvalidOnionVersion
×
755

756
                case sphinx.ErrInvalidOnionKey:
×
757
                        return lnwire.CodeInvalidOnionKey
×
758

759
                default:
×
760
                        log.Errorf("unable to decode onion packet: %v", err)
×
761
                        return lnwire.CodeInvalidOnionKey
×
762
                }
763

764
                var opts []sphinx.ProcessOnionOpt
3✔
765
                req.BlindingPoint.WhenSome(func(
3✔
766
                        b tlv.RecordT[lnwire.BlindingPointTlvType,
3✔
767
                                *btcec.PublicKey]) {
6✔
768

3✔
769
                        opts = append(opts, sphinx.WithBlindingPoint(
3✔
770
                                b.Val,
3✔
771
                        ))
3✔
772
                })
3✔
773

774
                // TODO(yy): use `p.router.ProcessOnionPacket` instead.
775
                err = tx.ProcessOnionPacket(
3✔
776
                        seqNum, onionPkt, req.RHash, req.IncomingCltv, opts...,
3✔
777
                )
3✔
778
                switch err {
3✔
779
                case nil:
3✔
780
                        // success
3✔
781
                        return lnwire.CodeNone
3✔
782

783
                case sphinx.ErrInvalidOnionVersion:
×
784
                        return lnwire.CodeInvalidOnionVersion
×
785

786
                case sphinx.ErrInvalidOnionHMAC:
×
787
                        return lnwire.CodeInvalidOnionHmac
×
788

789
                case sphinx.ErrInvalidOnionKey:
×
790
                        return lnwire.CodeInvalidOnionKey
×
791

792
                default:
×
793
                        log.Errorf("unable to process onion packet: %v", err)
×
794
                        return lnwire.CodeInvalidOnionKey
×
795
                }
796
        }
797

798
        // Execute cpu-heavy onion decoding in parallel.
799
        var wg sync.WaitGroup
3✔
800
        for i := range reqs {
6✔
801
                wg.Add(1)
3✔
802
                go func(seqNum uint16) {
6✔
803
                        defer wg.Done()
3✔
804

3✔
805
                        onionPkt := &onionPkts[seqNum]
3✔
806

3✔
807
                        resps[seqNum].FailCode = decode(
3✔
808
                                seqNum, onionPkt, reqs[seqNum],
3✔
809
                        )
3✔
810
                }(uint16(i))
3✔
811
        }
812
        wg.Wait()
3✔
813

3✔
814
        // With that batch created, we will now attempt to write the shared
3✔
815
        // secrets to disk. This operation will returns the set of indices that
3✔
816
        // were detected as replays, and the computed sphinx packets for all
3✔
817
        // indices that did not fail the above loop. Only indices that are not
3✔
818
        // in the replay set should be considered valid, as they are
3✔
819
        // opportunistically computed.
3✔
820
        packets, replays, err := tx.Commit()
3✔
821
        if err != nil {
3✔
822
                log.Errorf("unable to process onion packet batch %x: %v",
×
823
                        id, err)
×
824

×
825
                // If we failed to commit the batch to the secret share log, we
×
826
                // will mark all not-yet-failed channels with a temporary
×
827
                // channel failure and exit since we cannot proceed.
×
828
                for i := range resps {
×
829
                        resp := &resps[i]
×
830

×
831
                        // Skip any indexes that already failed onion decoding.
×
832
                        if resp.FailCode != lnwire.CodeNone {
×
833
                                continue
×
834
                        }
835

836
                        log.Errorf("unable to process onion packet %x-%v",
×
837
                                id, i)
×
838
                        resp.FailCode = lnwire.CodeTemporaryChannelFailure
×
839
                }
840

841
                // TODO(conner): return real errors to caller so link can fail?
842
                return resps, err
×
843
        }
844

845
        // Otherwise, the commit was successful. Now we will post process any
846
        // remaining packets, additionally failing any that were included in the
847
        // replay set.
848
        for i := range resps {
6✔
849
                resp := &resps[i]
3✔
850

3✔
851
                // Skip any indexes that already failed onion decoding.
3✔
852
                if resp.FailCode != lnwire.CodeNone {
3✔
853
                        continue
×
854
                }
855

856
                // If this index is contained in the replay set, and it is not a
857
                // reforwarding on startup, mark it with a permanent channel
858
                // failure error code. We infer that the offending error was due
859
                // to a replayed packet because this index was found in the
860
                // replay set.
861
                if !reforward && replays.Contains(uint16(i)) {
6✔
862
                        log.Errorf("unable to process onion packet: %v",
3✔
863
                                sphinx.ErrReplayedPacket)
3✔
864

3✔
865
                        // We set FailCode to CodeInvalidOnionVersion even
3✔
866
                        // though the ephemeral key isn't the problem. We need
3✔
867
                        // to set the BADONION bit since we're sending back a
3✔
868
                        // malformed packet, but as there isn't a specific
3✔
869
                        // failure code for replays, we reuse one of the
3✔
870
                        // failure codes that has BADONION.
3✔
871
                        resp.FailCode = lnwire.CodeInvalidOnionVersion
3✔
872
                        continue
3✔
873
                }
874

875
                // Finally, construct a hop iterator from our processed sphinx
876
                // packet, simultaneously caching the original onion packet.
877
                resp.HopIterator = makeSphinxHopIterator(
3✔
878
                        p.router, &onionPkts[i], &packets[i], BlindingKit{
3✔
879
                                Processor:         p.router,
3✔
880
                                UpdateAddBlinding: reqs[i].BlindingPoint,
3✔
881
                                IncomingAmount:    reqs[i].IncomingAmount,
3✔
882
                                IncomingCltv:      reqs[i].IncomingCltv,
3✔
883
                        }, reqs[i].RHash,
3✔
884
                )
3✔
885
        }
886

887
        return resps, nil
3✔
888
}
889

890
// ExtractSharedSecret takes an ephemeral session key as original received by a
891
// forwarding node and generates the shared secret. In the case that en error
892
// occurs, a lnwire failure code detailing the parsing failure will be returned.
893
func (p *OnionProcessor) ExtractSharedSecret(ephemeralKey *btcec.PublicKey) (
894
        sphinx.Hash256, lnwire.FailCode) {
36✔
895

36✔
896
        sharedSecret, err := p.router.GenerateSharedSecret(ephemeralKey, nil)
36✔
897
        if err != nil {
36✔
898
                switch err {
×
899
                case sphinx.ErrInvalidOnionVersion:
×
NEW
900
                        return sphinx.Hash256{}, lnwire.CodeInvalidOnionVersion
×
901
                case sphinx.ErrInvalidOnionHMAC:
×
NEW
902
                        return sphinx.Hash256{}, lnwire.CodeInvalidOnionHmac
×
903
                case sphinx.ErrInvalidOnionKey:
×
NEW
904
                        return sphinx.Hash256{}, lnwire.CodeInvalidOnionKey
×
905
                default:
×
906
                        log.Errorf("unable to process onion packet: %v", err)
×
NEW
907
                        return sphinx.Hash256{}, lnwire.CodeInvalidOnionKey
×
908
                }
909
        }
910

911
        return sharedSecret, lnwire.CodeNone
36✔
912
}
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