Skip to content

Commit 32cdbb4

Browse files
authored
Merge pull request #9454 from ziggie1984/add_custom_error_msg
Add Custom Error msg and Prioritise replayed HTLCs
2 parents f25e447 + f4e2f2a commit 32cdbb4

16 files changed

+579
-338
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Bug Fixes
2+
3+
* [Improved user experience](https://github.com/lightningnetwork/lnd/pull/9454)
4+
by returning a custom error code when HTLC carries incorrect custom records.
5+
6+
# Contributors (Alphabetical Order)
7+
8+
* Ziggie

htlcswitch/interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,4 +508,9 @@ type AuxTrafficShaper interface {
508508
PaymentBandwidth(htlcBlob, commitmentBlob fn.Option[tlv.Blob],
509509
linkBandwidth,
510510
htlcAmt lnwire.MilliSatoshi) (lnwire.MilliSatoshi, error)
511+
512+
// IsCustomHTLC returns true if the HTLC carries the set of relevant
513+
// custom records to put it under the purview of the traffic shaper,
514+
// meaning that it's from a custom channel.
515+
IsCustomHTLC(htlcRecords lnwire.CustomRecords) bool
511516
}

htlcswitch/link.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4164,21 +4164,20 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC,
41644164
return nil
41654165
}
41664166

4167+
// In case the traffic shaper is active, we'll check if the HTLC has
4168+
// custom records and skip the amount check in the onion payload below.
4169+
isCustomHTLC := fn.MapOptionZ(
4170+
l.cfg.AuxTrafficShaper,
4171+
func(ts AuxTrafficShaper) bool {
4172+
return ts.IsCustomHTLC(add.CustomRecords)
4173+
},
4174+
)
4175+
41674176
// As we're the exit hop, we'll double check the hop-payload included in
41684177
// the HTLC to ensure that it was crafted correctly by the sender and
4169-
// is compatible with the HTLC we were extended.
4170-
//
4171-
// For a special case, if the fwdInfo doesn't have any blinded path
4172-
// information, and the incoming HTLC had special extra data, then
4173-
// we'll skip this amount check. The invoice acceptor will make sure we
4174-
// reject the HTLC if it's not containing the correct amount after
4175-
// examining the custom data.
4176-
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
4177-
customHTLC := len(add.CustomRecords) > 0 && !hasBlindedPath
4178-
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
4179-
hasBlindedPath, customHTLC)
4180-
4181-
if !customHTLC && add.Amount < fwdInfo.AmountToForward {
4178+
// is compatible with the HTLC we were extended. If an external
4179+
// validator is active we might bypass the amount check.
4180+
if !isCustomHTLC && add.Amount < fwdInfo.AmountToForward {
41824181
l.log.Errorf("onion payload of incoming htlc(%x) has "+
41834182
"incompatible value: expected <=%v, got %v",
41844183
add.PaymentHash, add.Amount, fwdInfo.AmountToForward)

invoices/invoiceregistry.go

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/lightningnetwork/lnd/clock"
12+
"github.com/lightningnetwork/lnd/fn/v2"
1213
"github.com/lightningnetwork/lnd/lntypes"
1314
"github.com/lightningnetwork/lnd/lnwire"
1415
"github.com/lightningnetwork/lnd/queue"
@@ -653,56 +654,38 @@ func (i *InvoiceRegistry) startHtlcTimer(invoiceRef InvoiceRef,
653654
func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
654655
key CircuitKey, result FailResolutionResult) error {
655656

656-
updateInvoice := func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
657+
updateInvoice := func(invoice *Invoice, setID *SetID) (
658+
*InvoiceUpdateDesc, error) {
659+
657660
// Only allow individual htlc cancellation on open invoices.
658661
if invoice.State != ContractOpen {
659-
log.Debugf("cancelSingleHtlc: invoice %v no longer "+
660-
"open", invoiceRef)
662+
log.Debugf("CancelSingleHtlc: cannot cancel htlc %v "+
663+
"on invoice %v, invoice is no longer open", key,
664+
invoiceRef)
661665

662666
return nil, nil
663667
}
664668

665-
// Lookup the current status of the htlc in the database.
666-
var (
667-
htlcState HtlcState
668-
setID *SetID
669-
)
669+
// Also for AMP invoices we fetch the relevant HTLCs, so
670+
// the HTLC should be found, otherwise we return an error.
670671
htlc, ok := invoice.Htlcs[key]
671672
if !ok {
672-
// If this is an AMP invoice, then all the HTLCs won't
673-
// be read out, so we'll consult the other mapping to
674-
// try to find the HTLC state in question here.
675-
var found bool
676-
for ampSetID, htlcSet := range invoice.AMPState {
677-
ampSetID := ampSetID
678-
for htlcKey := range htlcSet.InvoiceKeys {
679-
if htlcKey == key {
680-
htlcState = htlcSet.State
681-
setID = &ampSetID
682-
683-
found = true
684-
break
685-
}
686-
}
687-
}
688-
689-
if !found {
690-
return nil, fmt.Errorf("htlc %v not found", key)
691-
}
692-
} else {
693-
htlcState = htlc.State
673+
return nil, fmt.Errorf("htlc %v not found on "+
674+
"invoice %v", key, invoiceRef)
694675
}
695676

677+
htlcState := htlc.State
678+
696679
// Cancellation is only possible if the htlc wasn't already
697680
// resolved.
698681
if htlcState != HtlcStateAccepted {
699-
log.Debugf("cancelSingleHtlc: htlc %v on invoice %v "+
682+
log.Debugf("CancelSingleHtlc: htlc %v on invoice %v "+
700683
"is already resolved", key, invoiceRef)
701684

702685
return nil, nil
703686
}
704687

705-
log.Debugf("cancelSingleHtlc: cancelling htlc %v on invoice %v",
688+
log.Debugf("CancelSingleHtlc: cancelling htlc %v on invoice %v",
706689
key, invoiceRef)
707690

708691
// Return an update descriptor that cancels htlc and keeps
@@ -728,7 +711,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
728711
func(invoice *Invoice) (
729712
*InvoiceUpdateDesc, error) {
730713

731-
updateDesc, err := updateInvoice(invoice)
714+
updateDesc, err := updateInvoice(invoice, setID)
732715
if err != nil {
733716
return nil, err
734717
}
@@ -755,8 +738,13 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
755738
key, int32(htlc.AcceptHeight), result,
756739
)
757740

741+
log.Debugf("Signaling htlc(%v) cancellation of invoice(%v) "+
742+
"with resolution(%v) to the link subsystem", key,
743+
invoiceRef, result)
744+
758745
i.notifyHodlSubscribers(resolution)
759746
}
747+
760748
return nil
761749
}
762750

@@ -1086,29 +1074,83 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
10861074
updateSubscribers bool
10871075
)
10881076
callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) {
1089-
updateDesc, res, err := updateInvoice(ctx, inv)
1077+
// First check if this is a replayed htlc and resolve it
1078+
// according to its current state. We cannot decide differently
1079+
// once the HTLC has already been processed before.
1080+
isReplayed, res, err := resolveReplayedHtlc(ctx, inv)
10901081
if err != nil {
10911082
return nil, err
10921083
}
1084+
if isReplayed {
1085+
resolution = res
1086+
return nil, nil
1087+
}
10931088

1094-
// Only send an update if the invoice state was changed.
1095-
updateSubscribers = updateDesc != nil &&
1096-
updateDesc.State != nil
1097-
1098-
// Assign resolution to outer scope variable.
1089+
// In case the HTLC interceptor cancels the HTLC set, we do NOT
1090+
// cancel the invoice however we cancel the complete HTLC set.
10991091
if cancelSet {
1100-
// If a cancel signal was set for the htlc set, we set
1101-
// the resolution as a failure with an underpayment
1102-
// indication. Something was wrong with this htlc, so
1103-
// we probably can't settle the invoice at all.
1092+
// If the invoice is not open, something is wrong, we
1093+
// fail just the HTLC with the specific error.
1094+
if inv.State != ContractOpen {
1095+
log.Errorf("Invoice state (%v) is not OPEN, "+
1096+
"cancelling HTLC set not allowed by "+
1097+
"external source", inv.State)
1098+
1099+
resolution = NewFailResolution(
1100+
ctx.circuitKey, ctx.currentHeight,
1101+
ResultInvoiceNotOpen,
1102+
)
1103+
1104+
return nil, nil
1105+
}
1106+
1107+
// The error `ExternalValidationFailed` error
1108+
// information will be packed in the
1109+
// `FailIncorrectDetails` msg when sending the msg to
1110+
// the peer. Error codes are defined by the BOLT 04
1111+
// specification. The error text can be arbitrary
1112+
// therefore we return a custom error msg.
11041113
resolution = NewFailResolution(
11051114
ctx.circuitKey, ctx.currentHeight,
1106-
ResultAmountTooLow,
1115+
ExternalValidationFailed,
11071116
)
1108-
} else {
1109-
resolution = res
1117+
1118+
// We cancel all HTLCs which are in the accepted state.
1119+
//
1120+
// NOTE: The current HTLC is not included because it
1121+
// was never accepted in the first place.
1122+
htlcs := inv.HTLCSet(ctx.setID(), HtlcStateAccepted)
1123+
htlcKeys := fn.KeySet[CircuitKey](htlcs)
1124+
1125+
// The external source did cancel the htlc set, so we
1126+
// cancel all HTLCs in the set. We however keep the
1127+
// invoice in the open state.
1128+
//
1129+
// NOTE: The invoice event loop will still call the
1130+
// `cancelSingleHTLC` method for MPP payments, however
1131+
// because the HTLCs are already cancled back it will be
1132+
// a NOOP.
1133+
update := &InvoiceUpdateDesc{
1134+
UpdateType: CancelHTLCsUpdate,
1135+
CancelHtlcs: htlcKeys,
1136+
SetID: setID,
1137+
}
1138+
1139+
return update, nil
11101140
}
11111141

1142+
updateDesc, res, err := updateInvoice(ctx, inv)
1143+
if err != nil {
1144+
return nil, err
1145+
}
1146+
1147+
// Set resolution in outer scope only after successful update.
1148+
resolution = res
1149+
1150+
// Only send an update if the invoice state was changed.
1151+
updateSubscribers = updateDesc != nil &&
1152+
updateDesc.State != nil
1153+
11121154
return updateDesc, nil
11131155
}
11141156

@@ -1417,6 +1459,8 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
14171459
}
14181460

14191461
invoiceRef := InvoiceRefByHash(payHash)
1462+
1463+
// We pass a nil setID which means no HTLCs will be read out.
14201464
invoice, err := i.idb.UpdateInvoice(ctx, invoiceRef, nil, updateInvoice)
14211465

14221466
// Implement idempotency by returning success if the invoice was already
@@ -1443,6 +1487,8 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
14431487
// that are waiting for resolution. Any htlcs that were already canceled
14441488
// before, will be notified again. This isn't necessary but doesn't hurt
14451489
// either.
1490+
//
1491+
// TODO(ziggie): Also consider AMP HTLCs here.
14461492
for key, htlc := range invoice.Htlcs {
14471493
if htlc.State != HtlcStateCanceled {
14481494
continue

0 commit comments

Comments
 (0)