Skip to content

Commit 17e37bd

Browse files
committed
multi: introduce new traffic shaper method.
We introduce a new specific fail resolution error when the external HTLC interceptor denies the incoming HTLC. Moreover we introduce a new traffic shaper method which moves the implementation of asset HTLC to the external layers. Moreover itests are adopted to reflect this new change.
1 parent 9ee12ee commit 17e37bd

File tree

8 files changed

+37
-248
lines changed

8 files changed

+37
-248
lines changed

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,13 +1117,15 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
11171117
return nil, nil
11181118
}
11191119

1120-
// If a cancel signal was set for the htlc set, we set
1121-
// the resolution as a failure with an underpayment
1122-
// indication. Something was wrong with this htlc, so
1123-
// we probably can't settle the invoice at all.
1120+
// The error `ExternalValidationFailed` error
1121+
// information will be packed in the
1122+
// `FailIncorrectDetails` msg when sending the msg to
1123+
// the peer. Error codes are defined by the BOLT 04
1124+
// specification. The error text can be arbitrary
1125+
// therefore we return a custom error msg.
11241126
resolution = NewFailResolution(
11251127
ctx.circuitKey, ctx.currentHeight,
1126-
ResultAmountTooLow,
1128+
ExternalValidationFailed,
11271129
)
11281130

11291131
// We cancel all HTLCs which are in the accepted state.

invoices/modification_interceptor.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,6 @@ func (s *HtlcModificationInterceptor) Intercept(clientRequest HtlcModifyRequest,
137137
// Wait for the client to respond or an error to occur.
138138
select {
139139
case response := <-responseChan:
140-
log.Debugf("Received invoice HTLC interceptor response: %v",
141-
response)
142-
143140
responseCallback(*response)
144141

145142
return nil

invoices/resolution_result.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ const (
120120
// ResultAmpReconstruction is returned when the derived child
121121
// hash/preimage pairs were invalid for at least one HTLC in the set.
122122
ResultAmpReconstruction
123+
124+
// ExternalValidationFailed is returned when the external validation
125+
// failed.
126+
ExternalValidationFailed
123127
)
124128

125129
// String returns a string representation of the result.
@@ -189,6 +193,9 @@ func (f FailResolutionResult) FailureString() string {
189193
case ResultAmpReconstruction:
190194
return "amp reconstruction failed"
191195

196+
case ExternalValidationFailed:
197+
return "external validation failed"
198+
192199
default:
193200
return "unknown failure resolution result"
194201
}
@@ -202,7 +209,8 @@ func (f FailResolutionResult) IsSetFailure() bool {
202209
ResultAmpReconstruction,
203210
ResultHtlcSetTotalTooLow,
204211
ResultHtlcSetTotalMismatch,
205-
ResultHtlcSetOverpayment:
212+
ResultHtlcSetOverpayment,
213+
ExternalValidationFailed:
206214

207215
return true
208216

itest/list_on_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -406,14 +406,6 @@ var allTestCases = []*lntest.TestCase{
406406
Name: "forward interceptor",
407407
TestFunc: testForwardInterceptorBasic,
408408
},
409-
{
410-
Name: "forward interceptor modified htlc",
411-
TestFunc: testForwardInterceptorModifiedHtlc,
412-
},
413-
{
414-
Name: "forward interceptor wire records",
415-
TestFunc: testForwardInterceptorWireRecords,
416-
},
417409
{
418410
Name: "forward interceptor restart",
419411
TestFunc: testForwardInterceptorRestart,

itest/lnd_forward_interceptor_test.go

Lines changed: 0 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/btcsuite/btcd/btcutil"
1111
"github.com/lightningnetwork/lnd/chainreg"
1212
"github.com/lightningnetwork/lnd/lnrpc"
13-
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
1413
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1514
"github.com/lightningnetwork/lnd/lntest"
1615
"github.com/lightningnetwork/lnd/lntest/node"
@@ -346,223 +345,6 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) {
346345
}
347346
}
348347

349-
// testForwardInterceptorModifiedHtlc tests that the interceptor can modify the
350-
// amount and custom records of an intercepted HTLC and resume it.
351-
func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
352-
const chanAmt = btcutil.Amount(300000)
353-
p := lntest.OpenChannelParams{Amt: chanAmt}
354-
355-
// Initialize the test context with 3 connected nodes.
356-
cfgs := [][]string{nil, nil, nil}
357-
358-
// Open and wait for channels.
359-
_, nodes := ht.CreateSimpleNetwork(cfgs, p)
360-
alice, bob, carol := nodes[0], nodes[1], nodes[2]
361-
362-
// Init the scenario.
363-
ts := &interceptorTestScenario{
364-
ht: ht,
365-
alice: alice,
366-
bob: bob,
367-
carol: carol,
368-
}
369-
370-
// Connect an interceptor to Bob's node.
371-
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
372-
373-
// We're going to modify the payment amount and want Carol to accept the
374-
// payment, so we set up an invoice acceptor on Dave.
375-
carolAcceptor, carolCancel := carol.RPC.InvoiceHtlcModifier()
376-
defer carolCancel()
377-
378-
// Prepare the test cases.
379-
invoiceValueAmtMsat := int64(20_000_000)
380-
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
381-
addResponse := carol.RPC.AddInvoice(req)
382-
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
383-
tc := &interceptorTestCase{
384-
amountMsat: invoiceValueAmtMsat,
385-
invoice: invoice,
386-
payAddr: invoice.PaymentAddr,
387-
}
388-
389-
// We initiate a payment from Alice.
390-
done := make(chan struct{})
391-
go func() {
392-
// Signal that all the payments have been sent.
393-
defer close(done)
394-
395-
ts.sendPaymentAndAssertAction(tc)
396-
}()
397-
398-
// We start the htlc interceptor with a simple implementation that saves
399-
// all intercepted packets. These packets are held to simulate a
400-
// pending payment.
401-
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
402-
403-
// Resume the intercepted HTLC with a modified amount and custom
404-
// records.
405-
customRecords := make(map[uint64][]byte)
406-
407-
// Add custom records entry.
408-
crKey := uint64(65537)
409-
crValue := []byte("custom-records-test-value")
410-
customRecords[crKey] = crValue
411-
412-
// Modify the amount of the HTLC, so we send out less than the original
413-
// amount.
414-
const modifyAmount = 5_000_000
415-
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
416-
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
417-
IncomingCircuitKey: packet.IncomingCircuitKey,
418-
OutAmountMsat: newOutAmountMsat,
419-
OutWireCustomRecords: customRecords,
420-
Action: actionResumeModify,
421-
})
422-
require.NoError(ht, err, "failed to send request")
423-
424-
invoicePacket := ht.ReceiveInvoiceHtlcModification(carolAcceptor)
425-
require.EqualValues(
426-
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
427-
)
428-
amtPaid := newOutAmountMsat + modifyAmount
429-
err = carolAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
430-
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
431-
AmtPaid: &amtPaid,
432-
})
433-
require.NoError(ht, err, "carol acceptor response")
434-
435-
// Cancel the context, which will disconnect Bob's interceptor.
436-
cancelBobInterceptor()
437-
438-
// Make sure all goroutines are finished.
439-
select {
440-
case <-done:
441-
case <-time.After(defaultTimeout):
442-
require.Fail(ht, "timeout waiting for sending payment")
443-
}
444-
445-
// Assert that the payment was successful.
446-
var preimage lntypes.Preimage
447-
copy(preimage[:], invoice.RPreimage)
448-
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
449-
}
450-
451-
// testForwardInterceptorWireRecords tests that the interceptor can read any
452-
// wire custom records provided by the sender of a payment as part of the
453-
// update_add_htlc message.
454-
func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
455-
const chanAmt = btcutil.Amount(300000)
456-
p := lntest.OpenChannelParams{Amt: chanAmt}
457-
458-
// Initialize the test context with 4 connected nodes.
459-
cfgs := [][]string{nil, nil, nil, nil}
460-
461-
// Open and wait for channels.
462-
_, nodes := ht.CreateSimpleNetwork(cfgs, p)
463-
alice, bob, carol, dave := nodes[0], nodes[1], nodes[2], nodes[3]
464-
465-
// Connect an interceptor to Bob's node.
466-
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
467-
defer cancelBobInterceptor()
468-
469-
// Also connect an interceptor on Carol's node to check whether we're
470-
// relaying the TLVs send in update_add_htlc over Alice -> Bob on the
471-
// Bob -> Carol link.
472-
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
473-
defer cancelCarolInterceptor()
474-
475-
// We're going to modify the payment amount and want Dave to accept the
476-
// payment, so we set up an invoice acceptor on Dave.
477-
daveAcceptor, daveCancel := dave.RPC.InvoiceHtlcModifier()
478-
defer daveCancel()
479-
480-
req := &lnrpc.Invoice{ValueMsat: 20_000_000}
481-
addResponse := dave.RPC.AddInvoice(req)
482-
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
483-
484-
customRecords := map[uint64][]byte{
485-
65537: []byte("test"),
486-
}
487-
sendReq := &routerrpc.SendPaymentRequest{
488-
PaymentRequest: invoice.PaymentRequest,
489-
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
490-
FeeLimitMsat: noFeeLimitMsat,
491-
FirstHopCustomRecords: customRecords,
492-
}
493-
ht.SendPaymentAssertInflight(alice, sendReq)
494-
495-
// We start the htlc interceptor with a simple implementation that saves
496-
// all intercepted packets. These packets are held to simulate a
497-
// pending payment.
498-
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
499-
require.Equal(ht, lntest.CustomRecordsWithUnendorsed(
500-
customRecords,
501-
), packet.InWireCustomRecords)
502-
503-
// Just resume the payment on Bob.
504-
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
505-
IncomingCircuitKey: packet.IncomingCircuitKey,
506-
Action: actionResume,
507-
})
508-
require.NoError(ht, err, "failed to send request")
509-
510-
// Assert that the Alice -> Bob custom records in update_add_htlc are
511-
// not propagated on the Bob -> Carol link, just an endorsement signal.
512-
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
513-
require.Equal(ht, lntest.CustomRecordsWithUnendorsed(nil),
514-
packet.InWireCustomRecords)
515-
516-
// We're going to tell Carol to forward 5k sats less to Dave. We need to
517-
// set custom records on the HTLC as well, to make sure the HTLC isn't
518-
// rejected outright and actually gets to the invoice acceptor.
519-
const modifyAmount = 5_000_000
520-
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
521-
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
522-
IncomingCircuitKey: packet.IncomingCircuitKey,
523-
OutAmountMsat: newOutAmountMsat,
524-
OutWireCustomRecords: customRecords,
525-
Action: actionResumeModify,
526-
})
527-
require.NoError(ht, err, "carol interceptor response")
528-
529-
// The payment should get to Dave, and we should be able to intercept
530-
// and modify it, telling Dave to accept it.
531-
invoicePacket := ht.ReceiveInvoiceHtlcModification(daveAcceptor)
532-
require.EqualValues(
533-
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
534-
)
535-
amtPaid := newOutAmountMsat + modifyAmount
536-
err = daveAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
537-
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
538-
AmtPaid: &amtPaid,
539-
})
540-
require.NoError(ht, err, "dave acceptor response")
541-
542-
// Assert that the payment was successful.
543-
var preimage lntypes.Preimage
544-
copy(preimage[:], invoice.RPreimage)
545-
ht.AssertPaymentStatus(
546-
alice, preimage, lnrpc.Payment_SUCCEEDED,
547-
func(p *lnrpc.Payment) error {
548-
recordsEqual := reflect.DeepEqual(
549-
p.FirstHopCustomRecords,
550-
lntest.CustomRecordsWithUnendorsed(
551-
customRecords,
552-
),
553-
)
554-
if !recordsEqual {
555-
return fmt.Errorf("expected custom records to "+
556-
"be equal, got %v expected %v",
557-
p.FirstHopCustomRecords,
558-
sendReq.FirstHopCustomRecords)
559-
}
560-
561-
return nil
562-
},
563-
)
564-
}
565-
566348
// testForwardInterceptorRestart tests that the interceptor can read any wire
567349
// custom records provided by the sender of a payment as part of the
568350
// update_add_htlc message and that those records are persisted correctly and

routing/bandwidth_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,7 @@ func (*mockTrafficShaper) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi,
164164

165165
return totalAmount, nil, nil
166166
}
167+
168+
func (*mockTrafficShaper) IsCustomHTLC(_ lnwire.CustomRecords) bool {
169+
return false
170+
}

0 commit comments

Comments
 (0)