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

lightningnetwork / lnd / 15838907453

24 Jun 2025 01:26AM UTC coverage: 57.079% (-11.1%) from 68.172%
15838907453

Pull #9982

github

web-flow
Merge e42780be2 into 45c15646c
Pull Request #9982: lnwire+lnwallet: add LocalNonces field for splice nonce coordination w/ taproot channels

103 of 167 new or added lines in 5 files covered. (61.68%)

30191 existing lines in 463 files now uncovered.

96331 of 168768 relevant lines covered (57.08%)

0.6 hits per line

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

53.35
/htlcswitch/hop/payload.go
1
package hop
2

3
import (
4
        "encoding/binary"
5
        "fmt"
6
        "io"
7

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

16
// PayloadViolation is an enum encapsulating the possible invalid payload
17
// violations that can occur when processing or validating a payload.
18
type PayloadViolation byte
19

20
const (
21
        // OmittedViolation indicates that a type was expected to be found the
22
        // payload but was absent.
23
        OmittedViolation PayloadViolation = iota
24

25
        // IncludedViolation indicates that a type was expected to be omitted
26
        // from the payload but was present.
27
        IncludedViolation
28

29
        // RequiredViolation indicates that an unknown even type was found in
30
        // the payload that we could not process.
31
        RequiredViolation
32

33
        // InsufficientViolation indicates that the provided type does
34
        // not satisfy constraints.
35
        InsufficientViolation
36
)
37

38
// String returns a human-readable description of the violation as a verb.
39
func (v PayloadViolation) String() string {
×
40
        switch v {
×
41
        case OmittedViolation:
×
42
                return "omitted"
×
43

44
        case IncludedViolation:
×
45
                return "included"
×
46

47
        case RequiredViolation:
×
48
                return "required"
×
49

50
        case InsufficientViolation:
×
51
                return "insufficient"
×
52

53
        default:
×
54
                return "unknown violation"
×
55
        }
56
}
57

58
// ErrInvalidPayload is an error returned when a parsed onion payload either
59
// included or omitted incorrect records for a particular hop type.
60
type ErrInvalidPayload struct {
61
        // Type the record's type that cause the violation.
62
        Type tlv.Type
63

64
        // Violation is an enum indicating the type of violation detected in
65
        // processing Type.
66
        Violation PayloadViolation
67

68
        // FinalHop if true, indicates that the violation is for the final hop
69
        // in the route (identified by next hop id), otherwise the violation is
70
        // for an intermediate hop.
71
        FinalHop bool
72
}
73

74
// Error returns a human-readable description of the invalid payload error.
75
func (e ErrInvalidPayload) Error() string {
×
76
        hopType := "intermediate"
×
77
        if e.FinalHop {
×
78
                hopType = "final"
×
79
        }
×
80

81
        return fmt.Sprintf("onion payload for %s hop %v record with type %d",
×
82
                hopType, e.Violation, e.Type)
×
83
}
84

85
// Payload encapsulates all information delivered to a hop in an onion payload.
86
// A Hop can represent either a TLV or legacy payload. The primary forwarding
87
// instruction can be accessed via ForwardingInfo, and additional records can be
88
// accessed by other member functions.
89
type Payload struct {
90
        // FwdInfo holds the basic parameters required for HTLC forwarding, e.g.
91
        // amount, cltv, and next hop.
92
        FwdInfo ForwardingInfo
93

94
        // MPP holds the info provided in an option_mpp record when parsed from
95
        // a TLV onion payload.
96
        MPP *record.MPP
97

98
        // AMP holds the info provided in an option_amp record when parsed from
99
        // a TLV onion payload.
100
        AMP *record.AMP
101

102
        // customRecords are user-defined records in the custom type range that
103
        // were included in the payload.
104
        customRecords record.CustomSet
105

106
        // encryptedData is a blob of data encrypted by the receiver for use
107
        // in blinded routes.
108
        encryptedData []byte
109

110
        // blindingPoint is an ephemeral pubkey for use in blinded routes.
111
        blindingPoint *btcec.PublicKey
112

113
        // metadata is additional data that is sent along with the payment to
114
        // the payee.
115
        metadata []byte
116

117
        // totalAmtMsat holds the info provided in total_amount_msat when
118
        // parsed from a TLV onion payload.
119
        totalAmtMsat lnwire.MilliSatoshi
120
}
121

122
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
123
// parameters provided by leegacy onion payloads.
UNCOV
124
func NewLegacyPayload(f *sphinx.HopData) *Payload {
×
UNCOV
125
        nextHop := binary.BigEndian.Uint64(f.NextAddress[:])
×
UNCOV
126

×
UNCOV
127
        return &Payload{
×
UNCOV
128
                FwdInfo: ForwardingInfo{
×
UNCOV
129
                        NextHop:         lnwire.NewShortChanIDFromInt(nextHop),
×
UNCOV
130
                        AmountToForward: lnwire.MilliSatoshi(f.ForwardAmount),
×
UNCOV
131
                        OutgoingCTLV:    f.OutgoingCltv,
×
UNCOV
132
                },
×
UNCOV
133
                customRecords: make(record.CustomSet),
×
UNCOV
134
        }
×
UNCOV
135
}
×
136

137
// ParseTLVPayload builds a new Hop from the passed io.Reader and returns
138
// a map of all the types that were found in the payload. This function
139
// does not perform validation of TLV types included in the payload.
140
func ParseTLVPayload(r io.Reader) (*Payload, map[tlv.Type][]byte, error) {
1✔
141
        var (
1✔
142
                cid           uint64
1✔
143
                amt           uint64
1✔
144
                totalAmtMsat  uint64
1✔
145
                cltv          uint32
1✔
146
                mpp           = &record.MPP{}
1✔
147
                amp           = &record.AMP{}
1✔
148
                encryptedData []byte
1✔
149
                blindingPoint *btcec.PublicKey
1✔
150
                metadata      []byte
1✔
151
        )
1✔
152

1✔
153
        tlvStream, err := tlv.NewStream(
1✔
154
                record.NewAmtToFwdRecord(&amt),
1✔
155
                record.NewLockTimeRecord(&cltv),
1✔
156
                record.NewNextHopIDRecord(&cid),
1✔
157
                mpp.Record(),
1✔
158
                record.NewEncryptedDataRecord(&encryptedData),
1✔
159
                record.NewBlindingPointRecord(&blindingPoint),
1✔
160
                amp.Record(),
1✔
161
                record.NewMetadataRecord(&metadata),
1✔
162
                record.NewTotalAmtMsatBlinded(&totalAmtMsat),
1✔
163
        )
1✔
164
        if err != nil {
1✔
165
                return nil, nil, err
×
166
        }
×
167

168
        // Since this data is provided by a potentially malicious peer, pass it
169
        // into the P2P decoding variant.
170
        parsedTypes, err := tlvStream.DecodeWithParsedTypesP2P(r)
1✔
171
        if err != nil {
1✔
UNCOV
172
                return nil, nil, err
×
UNCOV
173
        }
×
174

175
        // If no MPP field was parsed, set the MPP field on the resulting
176
        // payload to nil.
177
        if _, ok := parsedTypes[record.MPPOnionType]; !ok {
2✔
178
                mpp = nil
1✔
179
        }
1✔
180

181
        // If no AMP field was parsed, set the MPP field on the resulting
182
        // payload to nil.
183
        if _, ok := parsedTypes[record.AMPOnionType]; !ok {
2✔
184
                amp = nil
1✔
185
        }
1✔
186

187
        // If no encrypted data was parsed, set the field on our resulting
188
        // payload to nil.
189
        if _, ok := parsedTypes[record.EncryptedDataOnionType]; !ok {
2✔
190
                encryptedData = nil
1✔
191
        }
1✔
192

193
        // If no metadata field was parsed, set the metadata field on the
194
        // resulting payload to nil.
195
        if _, ok := parsedTypes[record.MetadataOnionType]; !ok {
2✔
196
                metadata = nil
1✔
197
        }
1✔
198

199
        // Filter out the custom records.
200
        customRecords := NewCustomRecords(parsedTypes)
1✔
201

1✔
202
        return &Payload{
1✔
203
                FwdInfo: ForwardingInfo{
1✔
204
                        NextHop:         lnwire.NewShortChanIDFromInt(cid),
1✔
205
                        AmountToForward: lnwire.MilliSatoshi(amt),
1✔
206
                        OutgoingCTLV:    cltv,
1✔
207
                },
1✔
208
                MPP:           mpp,
1✔
209
                AMP:           amp,
1✔
210
                metadata:      metadata,
1✔
211
                encryptedData: encryptedData,
1✔
212
                blindingPoint: blindingPoint,
1✔
213
                customRecords: customRecords,
1✔
214
                totalAmtMsat:  lnwire.MilliSatoshi(totalAmtMsat),
1✔
215
        }, parsedTypes, nil
1✔
216
}
217

218
// ValidateTLVPayload validates the TLV fields that were included in a TLV
219
// payload.
220
func ValidateTLVPayload(parsedTypes map[tlv.Type][]byte,
221
        finalHop bool, updateAddBlinding bool) error {
1✔
222

1✔
223
        // Validate whether the sender properly included or omitted tlv records
1✔
224
        // in accordance with BOLT 04.
1✔
225
        err := ValidateParsedPayloadTypes(
1✔
226
                parsedTypes, finalHop, updateAddBlinding,
1✔
227
        )
1✔
228
        if err != nil {
1✔
UNCOV
229
                return err
×
UNCOV
230
        }
×
231

232
        // Check for violation of the rules for mandatory fields.
233
        violatingType := getMinRequiredViolation(parsedTypes)
1✔
234
        if violatingType != nil {
1✔
UNCOV
235
                return ErrInvalidPayload{
×
UNCOV
236
                        Type:      *violatingType,
×
UNCOV
237
                        Violation: RequiredViolation,
×
UNCOV
238
                        FinalHop:  finalHop,
×
UNCOV
239
                }
×
UNCOV
240
        }
×
241

242
        return nil
1✔
243
}
244

245
// ForwardingInfo returns the basic parameters required for HTLC forwarding,
246
// e.g. amount, cltv, and next hop.
247
func (h *Payload) ForwardingInfo() ForwardingInfo {
1✔
248
        return h.FwdInfo
1✔
249
}
1✔
250

251
// NewCustomRecords filters the types parsed from the tlv stream for custom
252
// records.
253
func NewCustomRecords(parsedTypes tlv.TypeMap) record.CustomSet {
1✔
254
        customRecords := make(record.CustomSet)
1✔
255
        for t, parseResult := range parsedTypes {
2✔
256
                if parseResult == nil || t < record.CustomTypeStart {
2✔
257
                        continue
1✔
258
                }
259
                customRecords[uint64(t)] = parseResult
1✔
260
        }
261
        return customRecords
1✔
262
}
263

264
// ValidateParsedPayloadTypes checks the types parsed from a hop payload to
265
// ensure that the proper fields are either included or omitted. The finalHop
266
// boolean should be true if the payload was parsed for an exit hop. The
267
// requirements for this method are described in BOLT 04.
268
func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap,
269
        isFinalHop, updateAddBlinding bool) error {
1✔
270

1✔
271
        _, hasAmt := parsedTypes[record.AmtOnionType]
1✔
272
        _, hasLockTime := parsedTypes[record.LockTimeOnionType]
1✔
273
        _, hasNextHop := parsedTypes[record.NextHopOnionType]
1✔
274
        _, hasMPP := parsedTypes[record.MPPOnionType]
1✔
275
        _, hasAMP := parsedTypes[record.AMPOnionType]
1✔
276
        _, hasEncryptedData := parsedTypes[record.EncryptedDataOnionType]
1✔
277
        _, hasBlinding := parsedTypes[record.BlindingPointOnionType]
1✔
278

1✔
279
        // All cleartext hops (including final hop) and the final hop in a
1✔
280
        // blinded path require the forwading amount and expiry TLVs to be set.
1✔
281
        needFwdInfo := isFinalHop || !hasEncryptedData
1✔
282

1✔
283
        // No blinded hops should have a next hop specified, and only the final
1✔
284
        // hop in a cleartext route should exclude it.
1✔
285
        needNextHop := !(hasEncryptedData || isFinalHop)
1✔
286

1✔
287
        switch {
1✔
288
        // Both blinding point being set is invalid.
UNCOV
289
        case hasBlinding && updateAddBlinding:
×
UNCOV
290
                return ErrInvalidPayload{
×
UNCOV
291
                        Type:      record.BlindingPointOnionType,
×
UNCOV
292
                        Violation: IncludedViolation,
×
UNCOV
293
                        FinalHop:  isFinalHop,
×
UNCOV
294
                }
×
295

296
        // If encrypted data is not provided, blinding points should not be
297
        // set.
UNCOV
298
        case !hasEncryptedData && (hasBlinding || updateAddBlinding):
×
UNCOV
299
                return ErrInvalidPayload{
×
UNCOV
300
                        Type:      record.EncryptedDataOnionType,
×
UNCOV
301
                        Violation: OmittedViolation,
×
UNCOV
302
                        FinalHop:  isFinalHop,
×
UNCOV
303
                }
×
304

305
        // If encrypted data is present, we require that one blinding point
306
        // is set.
UNCOV
307
        case hasEncryptedData && !(hasBlinding || updateAddBlinding):
×
UNCOV
308
                return ErrInvalidPayload{
×
UNCOV
309
                        Type:      record.EncryptedDataOnionType,
×
UNCOV
310
                        Violation: IncludedViolation,
×
UNCOV
311
                        FinalHop:  isFinalHop,
×
UNCOV
312
                }
×
313

314
        // Hops that need forwarding info must include an amount to forward.
UNCOV
315
        case needFwdInfo && !hasAmt:
×
UNCOV
316
                return ErrInvalidPayload{
×
UNCOV
317
                        Type:      record.AmtOnionType,
×
UNCOV
318
                        Violation: OmittedViolation,
×
UNCOV
319
                        FinalHop:  isFinalHop,
×
UNCOV
320
                }
×
321

322
        // Hops that need forwarding info must include a cltv expiry.
UNCOV
323
        case needFwdInfo && !hasLockTime:
×
UNCOV
324
                return ErrInvalidPayload{
×
UNCOV
325
                        Type:      record.LockTimeOnionType,
×
UNCOV
326
                        Violation: OmittedViolation,
×
UNCOV
327
                        FinalHop:  isFinalHop,
×
UNCOV
328
                }
×
329

330
        // Hops that don't need forwarding info shouldn't have an amount TLV.
UNCOV
331
        case !needFwdInfo && hasAmt:
×
UNCOV
332
                return ErrInvalidPayload{
×
UNCOV
333
                        Type:      record.AmtOnionType,
×
UNCOV
334
                        Violation: IncludedViolation,
×
UNCOV
335
                        FinalHop:  isFinalHop,
×
UNCOV
336
                }
×
337

338
        // Hops that don't need forwarding info shouldn't have a cltv TLV.
UNCOV
339
        case !needFwdInfo && hasLockTime:
×
UNCOV
340
                return ErrInvalidPayload{
×
UNCOV
341
                        Type:      record.LockTimeOnionType,
×
UNCOV
342
                        Violation: IncludedViolation,
×
UNCOV
343
                        FinalHop:  isFinalHop,
×
UNCOV
344
                }
×
345

346
        // The exit hop and all blinded hops should omit the next hop id.
UNCOV
347
        case !needNextHop && hasNextHop:
×
UNCOV
348
                return ErrInvalidPayload{
×
UNCOV
349
                        Type:      record.NextHopOnionType,
×
UNCOV
350
                        Violation: IncludedViolation,
×
UNCOV
351
                        FinalHop:  isFinalHop,
×
UNCOV
352
                }
×
353

354
        // Require that the next hop is set for intermediate hops in regular
355
        // routes.
UNCOV
356
        case needNextHop && !hasNextHop:
×
UNCOV
357
                return ErrInvalidPayload{
×
UNCOV
358
                        Type:      record.NextHopOnionType,
×
UNCOV
359
                        Violation: OmittedViolation,
×
UNCOV
360
                        FinalHop:  isFinalHop,
×
UNCOV
361
                }
×
362

363
        // Intermediate nodes should never receive MPP fields.
UNCOV
364
        case !isFinalHop && hasMPP:
×
UNCOV
365
                return ErrInvalidPayload{
×
UNCOV
366
                        Type:      record.MPPOnionType,
×
UNCOV
367
                        Violation: IncludedViolation,
×
UNCOV
368
                        FinalHop:  isFinalHop,
×
UNCOV
369
                }
×
370

371
        // Intermediate nodes should never receive AMP fields.
UNCOV
372
        case !isFinalHop && hasAMP:
×
UNCOV
373
                return ErrInvalidPayload{
×
UNCOV
374
                        Type:      record.AMPOnionType,
×
UNCOV
375
                        Violation: IncludedViolation,
×
UNCOV
376
                        FinalHop:  isFinalHop,
×
UNCOV
377
                }
×
378
        }
379

380
        return nil
1✔
381
}
382

383
// MultiPath returns the record corresponding the option_mpp parsed from the
384
// onion payload.
385
func (h *Payload) MultiPath() *record.MPP {
1✔
386
        return h.MPP
1✔
387
}
1✔
388

389
// AMPRecord returns the record corresponding with option_amp parsed from the
390
// onion payload.
391
func (h *Payload) AMPRecord() *record.AMP {
1✔
392
        return h.AMP
1✔
393
}
1✔
394

395
// CustomRecords returns the custom tlv type records that were parsed from the
396
// payload.
397
func (h *Payload) CustomRecords() record.CustomSet {
1✔
398
        return h.customRecords
1✔
399
}
1✔
400

401
// EncryptedData returns the route blinding encrypted data parsed from the
402
// onion payload.
UNCOV
403
func (h *Payload) EncryptedData() []byte {
×
UNCOV
404
        return h.encryptedData
×
UNCOV
405
}
×
406

407
// BlindingPoint returns the route blinding point parsed from the onion payload.
UNCOV
408
func (h *Payload) BlindingPoint() *btcec.PublicKey {
×
UNCOV
409
        return h.blindingPoint
×
UNCOV
410
}
×
411

412
// PathID returns the path ID that was encoded in the final hop payload of a
413
// blinded payment.
414
func (h *Payload) PathID() *chainhash.Hash {
1✔
415
        return h.FwdInfo.PathID
1✔
416
}
1✔
417

418
// Metadata returns the additional data that is sent along with the
419
// payment to the payee.
420
func (h *Payload) Metadata() []byte {
1✔
421
        return h.metadata
1✔
422
}
1✔
423

424
// TotalAmtMsat returns the total amount sent to the final hop, as set by the
425
// payee.
426
func (h *Payload) TotalAmtMsat() lnwire.MilliSatoshi {
1✔
427
        return h.totalAmtMsat
1✔
428
}
1✔
429

430
// getMinRequiredViolation checks for unrecognized required (even) fields in the
431
// standard range and returns the lowest required type. Always returning the
432
// lowest required type allows a failure message to be deterministic.
433
func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
1✔
434
        var (
1✔
435
                requiredViolation        bool
1✔
436
                minRequiredViolationType tlv.Type
1✔
437
        )
1✔
438
        for t, parseResult := range set {
2✔
439
                // If a type is even but not known to us, we cannot process the
1✔
440
                // payload. We are required to understand a field that we don't
1✔
441
                // support.
1✔
442
                //
1✔
443
                // We always accept custom fields, because a higher level
1✔
444
                // application may understand them.
1✔
445
                if parseResult == nil || t%2 != 0 ||
1✔
446
                        t >= record.CustomTypeStart {
2✔
447

1✔
448
                        continue
1✔
449
                }
450

UNCOV
451
                if !requiredViolation || t < minRequiredViolationType {
×
UNCOV
452
                        minRequiredViolationType = t
×
UNCOV
453
                }
×
UNCOV
454
                requiredViolation = true
×
455
        }
456

457
        if requiredViolation {
1✔
UNCOV
458
                return &minRequiredViolationType
×
UNCOV
459
        }
×
460

461
        return nil
1✔
462
}
463

464
// ValidateBlindedRouteData performs the additional validation that is
465
// required for payments that rely on data provided in an encrypted blob to
466
// be forwarded. We enforce the blinded route's maximum expiry height so that
467
// the route "expires" and a malicious party does not have endless opportunity
468
// to probe the blinded route and compare it to updated channel policies in
469
// the network.
470
func ValidateBlindedRouteData(blindedData *record.BlindedRouteData,
471
        incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error {
1✔
472

1✔
473
        // Bolt 04 notes that we should enforce payment constraints _if_ they
1✔
474
        // are present, so we do not fail if not provided.
1✔
475
        var err error
1✔
476
        blindedData.Constraints.WhenSome(
1✔
477
                func(c tlv.RecordT[tlv.TlvType12, record.PaymentConstraints]) {
2✔
478
                        // MUST fail if the expiry is greater than
1✔
479
                        // max_cltv_expiry.
1✔
480
                        if incomingTimelock > c.Val.MaxCltvExpiry {
1✔
UNCOV
481
                                err = ErrInvalidPayload{
×
UNCOV
482
                                        Type:      record.LockTimeOnionType,
×
UNCOV
483
                                        Violation: InsufficientViolation,
×
UNCOV
484
                                }
×
UNCOV
485
                        }
×
486

487
                        // MUST fail if the amount is below htlc_minimum_msat.
488
                        if incomingAmount < c.Val.HtlcMinimumMsat {
1✔
UNCOV
489
                                err = ErrInvalidPayload{
×
UNCOV
490
                                        Type:      record.AmtOnionType,
×
UNCOV
491
                                        Violation: InsufficientViolation,
×
UNCOV
492
                                }
×
UNCOV
493
                        }
×
494
                },
495
        )
496
        if err != nil {
1✔
UNCOV
497
                return err
×
UNCOV
498
        }
×
499

500
        // Fail if we don't understand any features (even or odd), because we
501
        // expect the features to have been set from our announcement. If the
502
        // feature vector TLV is not included, it's interpreted as an empty
503
        // vector (no validation required).
504
        // expect the features to have been set from our announcement.
505
        //
506
        // Note that we do not yet check the features that the blinded payment
507
        // is using against our own features, because there are currently no
508
        // payment-related features that they utilize other than tlv-onion,
509
        // which is implicitly supported.
510
        blindedData.Features.WhenSome(
1✔
511
                func(f tlv.RecordT[tlv.TlvType14, lnwire.FeatureVector]) {
1✔
UNCOV
512
                        if f.Val.UnknownFeatures() {
×
UNCOV
513
                                err = ErrInvalidPayload{
×
UNCOV
514
                                        Type:      14,
×
UNCOV
515
                                        Violation: IncludedViolation,
×
UNCOV
516
                                }
×
UNCOV
517
                        }
×
518
                },
519
        )
520
        if err != nil {
1✔
UNCOV
521
                return err
×
UNCOV
522
        }
×
523

524
        return nil
1✔
525
}
526

527
// ValidatePayloadWithBlinded validates a payload against the contents of
528
// its encrypted data blob.
529
func ValidatePayloadWithBlinded(isFinalHop bool,
530
        payloadParsed map[tlv.Type][]byte) error {
1✔
531

1✔
532
        // Blinded routes restrict the presence of TLVs more strictly than
1✔
533
        // regular routes, check that intermediate and final hops only have
1✔
534
        // the TLVs the spec allows them to have.
1✔
535
        allowedTLVs := map[tlv.Type]bool{
1✔
536
                record.EncryptedDataOnionType: true,
1✔
537
                record.BlindingPointOnionType: true,
1✔
538
        }
1✔
539

1✔
540
        if isFinalHop {
2✔
541
                allowedTLVs[record.AmtOnionType] = true
1✔
542
                allowedTLVs[record.LockTimeOnionType] = true
1✔
543
                allowedTLVs[record.TotalAmtMsatBlindedType] = true
1✔
544
        }
1✔
545

546
        for tlvType := range payloadParsed {
2✔
547
                if _, ok := allowedTLVs[tlvType]; ok {
2✔
548
                        continue
1✔
549
                }
550

UNCOV
551
                return ErrInvalidPayload{
×
UNCOV
552
                        Type:      tlvType,
×
UNCOV
553
                        Violation: IncludedViolation,
×
UNCOV
554
                        FinalHop:  isFinalHop,
×
UNCOV
555
                }
×
556
        }
557

558
        return nil
1✔
559
}
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