Skip to content

Commit a908c57

Browse files
authored
Merge pull request #8633 from ffranr/8619-rpc+htlcswitch-add-htlc-transformation-capabilities-to-the-interceptor
rpc+htlcswitch: add htlc transformation capabilities to the interceptor
2 parents 81970ea + 03dceca commit a908c57

22 files changed

+1588
-262
lines changed

htlcswitch/interceptable_switch.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"github.com/go-errors/errors"
99
"github.com/lightningnetwork/lnd/chainntnfs"
1010
"github.com/lightningnetwork/lnd/channeldb/models"
11+
"github.com/lightningnetwork/lnd/fn"
1112
"github.com/lightningnetwork/lnd/htlcswitch/hop"
1213
"github.com/lightningnetwork/lnd/lntypes"
1314
"github.com/lightningnetwork/lnd/lnwire"
15+
"github.com/lightningnetwork/lnd/record"
1416
)
1517

1618
var (
@@ -105,6 +107,10 @@ const (
105107

106108
// FwdActionFail fails the intercepted packet back to the sender.
107109
FwdActionFail
110+
111+
// FwdActionResumeModified forwards the intercepted packet to the switch
112+
// with modifications.
113+
FwdActionResumeModified
108114
)
109115

110116
// FwdResolution defines the action to be taken on an intercepted packet.
@@ -119,6 +125,18 @@ type FwdResolution struct {
119125
// FwdActionSettle.
120126
Preimage lntypes.Preimage
121127

128+
// IncomingAmountMsat is the amount that is to be used for validating if
129+
// Action is FwdActionResumeModified.
130+
IncomingAmountMsat fn.Option[lnwire.MilliSatoshi]
131+
132+
// OutgoingAmountMsat is the amount that is to be used for forwarding if
133+
// Action is FwdActionResumeModified.
134+
OutgoingAmountMsat fn.Option[lnwire.MilliSatoshi]
135+
136+
// CustomRecords is the custom records that are to be used for
137+
// forwarding if Action is FwdActionResumeModified.
138+
CustomRecords fn.Option[record.CustomSet]
139+
122140
// FailureMessage is the encrypted failure message that is to be passed
123141
// back to the sender if action is FwdActionFail.
124142
FailureMessage []byte
@@ -363,6 +381,8 @@ func (s *InterceptableSwitch) setInterceptor(interceptor ForwardInterceptor) {
363381
})
364382
}
365383

384+
// resolve processes a HTLC given the resolution type specified by the
385+
// intercepting client.
366386
func (s *InterceptableSwitch) resolve(res *FwdResolution) error {
367387
intercepted, err := s.heldHtlcSet.pop(res.Key)
368388
if err != nil {
@@ -373,6 +393,12 @@ func (s *InterceptableSwitch) resolve(res *FwdResolution) error {
373393
case FwdActionResume:
374394
return intercepted.Resume()
375395

396+
case FwdActionResumeModified:
397+
return intercepted.ResumeModified(
398+
res.IncomingAmountMsat, res.OutgoingAmountMsat,
399+
res.CustomRecords,
400+
)
401+
376402
case FwdActionSettle:
377403
return intercepted.Settle(res.Preimage)
378404

@@ -615,6 +641,73 @@ func (f *interceptedForward) Resume() error {
615641
return f.htlcSwitch.ForwardPackets(nil, f.packet)
616642
}
617643

644+
// ResumeModified resumes the default behavior with field modifications.
645+
func (f *interceptedForward) ResumeModified(
646+
incomingAmountMsat fn.Option[lnwire.MilliSatoshi],
647+
outgoingAmountMsat fn.Option[lnwire.MilliSatoshi],
648+
customRecords fn.Option[record.CustomSet]) error {
649+
650+
// Set the incoming amount, if it is provided, on the packet.
651+
incomingAmountMsat.WhenSome(func(amount lnwire.MilliSatoshi) {
652+
f.packet.incomingAmount = amount
653+
})
654+
655+
// Modify the wire message contained in the packet.
656+
switch htlc := f.packet.htlc.(type) {
657+
case *lnwire.UpdateAddHTLC:
658+
outgoingAmountMsat.WhenSome(func(amount lnwire.MilliSatoshi) {
659+
htlc.Amount = amount
660+
})
661+
662+
//nolint:lll
663+
err := fn.MapOptionZ(customRecords, func(records record.CustomSet) error {
664+
if len(records) == 0 {
665+
return nil
666+
}
667+
668+
// Type cast and validate custom records.
669+
htlc.CustomRecords = lnwire.CustomRecords(records)
670+
err := htlc.CustomRecords.Validate()
671+
if err != nil {
672+
return fmt.Errorf("failed to validate custom "+
673+
"records: %w", err)
674+
}
675+
676+
return nil
677+
})
678+
if err != nil {
679+
return fmt.Errorf("failed to encode custom records: %w",
680+
err)
681+
}
682+
683+
case *lnwire.UpdateFulfillHTLC:
684+
//nolint:lll
685+
err := fn.MapOptionZ(customRecords, func(records record.CustomSet) error {
686+
if len(records) == 0 {
687+
return nil
688+
}
689+
690+
// Type cast and validate custom records.
691+
htlc.CustomRecords = lnwire.CustomRecords(records)
692+
err := htlc.CustomRecords.Validate()
693+
if err != nil {
694+
return fmt.Errorf("failed to validate custom "+
695+
"records: %w", err)
696+
}
697+
698+
return nil
699+
})
700+
if err != nil {
701+
return fmt.Errorf("failed to encode custom records: %w",
702+
err)
703+
}
704+
}
705+
706+
// Forward to the switch. A link quit channel isn't needed, because we
707+
// are on a different thread now.
708+
return f.htlcSwitch.ForwardPackets(nil, f.packet)
709+
}
710+
618711
// Fail notifies the intention to Fail an existing hold forward with an
619712
// encrypted failure reason.
620713
func (f *interceptedForward) Fail(reason []byte) error {

htlcswitch/interfaces.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/btcsuite/btcd/wire"
77
"github.com/lightningnetwork/lnd/channeldb"
88
"github.com/lightningnetwork/lnd/channeldb/models"
9+
"github.com/lightningnetwork/lnd/fn"
910
"github.com/lightningnetwork/lnd/invoices"
1011
"github.com/lightningnetwork/lnd/lntypes"
1112
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -323,7 +324,7 @@ type InterceptableHtlcForwarder interface {
323324
type ForwardInterceptor func(InterceptedPacket) error
324325

325326
// InterceptedPacket contains the relevant information for the interceptor about
326-
// an htlc.
327+
// an HTLC.
327328
type InterceptedPacket struct {
328329
// IncomingCircuit contains the incoming channel and htlc id of the
329330
// packet.
@@ -375,6 +376,12 @@ type InterceptedForward interface {
375376
// this htlc which usually means forward it.
376377
Resume() error
377378

379+
// ResumeModified notifies the intention to resume an existing hold
380+
// forward with modified fields.
381+
ResumeModified(incomingAmountMsat,
382+
outgoingAmountMsat fn.Option[lnwire.MilliSatoshi],
383+
customRecords fn.Option[record.CustomSet]) error
384+
378385
// Settle notifies the intention to settle an existing hold
379386
// forward with a given preimage.
380387
Settle(lntypes.Preimage) error

htlcswitch/payment_result_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ func TestNetworkResultSerialization(t *testing.T) {
3838
ChanID: chanID,
3939
ID: 2,
4040
PaymentPreimage: preimage,
41-
ExtraData: make([]byte, 0),
4241
}
4342

4443
fail := &lnwire.UpdateFailHTLC{

intercepted_forward.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package lnd
33
import (
44
"errors"
55

6+
"github.com/lightningnetwork/lnd/fn"
67
"github.com/lightningnetwork/lnd/htlcswitch"
78
"github.com/lightningnetwork/lnd/lntypes"
89
"github.com/lightningnetwork/lnd/lnwire"
10+
"github.com/lightningnetwork/lnd/record"
911
)
1012

1113
var (
@@ -51,6 +53,14 @@ func (f *interceptedForward) Resume() error {
5153
return ErrCannotResume
5254
}
5355

56+
// ResumeModified notifies the intention to resume an existing hold forward with
57+
// a modified htlc.
58+
func (f *interceptedForward) ResumeModified(_, _ fn.Option[lnwire.MilliSatoshi],
59+
_ fn.Option[record.CustomSet]) error {
60+
61+
return ErrCannotResume
62+
}
63+
5464
// Fail notifies the intention to fail an existing hold forward with an
5565
// encrypted failure reason.
5666
func (f *interceptedForward) Fail(_ []byte) error {

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{
422422
Name: "forward interceptor",
423423
TestFunc: testForwardInterceptorBasic,
424424
},
425+
{
426+
Name: "forward interceptor modified htlc",
427+
TestFunc: testForwardInterceptorModifiedHtlc,
428+
},
425429
{
426430
Name: "zero conf channel open",
427431
TestFunc: testZeroConfChannelOpen,

itest/lnd_forward_interceptor_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package itest
22

33
import (
4+
"context"
45
"fmt"
56
"strings"
67
"time"
@@ -344,6 +345,140 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) {
344345
ht.CloseChannel(bob, cpBC)
345346
}
346347

348+
// testForwardInterceptorModifiedHtlc tests that the interceptor can modify the
349+
// amount and custom records of an intercepted HTLC and resume it.
350+
func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
351+
// Initialize the test context with 3 connected nodes.
352+
ts := newInterceptorTestScenario(ht)
353+
354+
alice, bob, carol := ts.alice, ts.bob, ts.carol
355+
356+
// Open and wait for channels.
357+
const chanAmt = btcutil.Amount(300000)
358+
p := lntest.OpenChannelParams{Amt: chanAmt}
359+
reqs := []*lntest.OpenChannelRequest{
360+
{Local: alice, Remote: bob, Param: p},
361+
{Local: bob, Remote: carol, Param: p},
362+
}
363+
resp := ht.OpenMultiChannelsAsync(reqs)
364+
cpAB, cpBC := resp[0], resp[1]
365+
366+
// Make sure Alice is aware of channel Bob=>Carol.
367+
ht.AssertTopologyChannelOpen(alice, cpBC)
368+
369+
// Connect an interceptor to Bob's node.
370+
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
371+
372+
// Prepare the test cases.
373+
invoiceValueAmtMsat := int64(1000)
374+
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
375+
addResponse := carol.RPC.AddInvoice(req)
376+
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
377+
tc := &interceptorTestCase{
378+
amountMsat: invoiceValueAmtMsat,
379+
invoice: invoice,
380+
payAddr: invoice.PaymentAddr,
381+
}
382+
383+
// We initiate a payment from Alice.
384+
done := make(chan struct{})
385+
go func() {
386+
// Signal that all the payments have been sent.
387+
defer close(done)
388+
389+
ts.sendPaymentAndAssertAction(tc)
390+
}()
391+
392+
// We start the htlc interceptor with a simple implementation that saves
393+
// all intercepted packets. These packets are held to simulate a
394+
// pending payment.
395+
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
396+
397+
// Resume the intercepted HTLC with a modified amount and custom
398+
// records.
399+
if packet.CustomRecords == nil {
400+
packet.CustomRecords = make(map[uint64][]byte)
401+
}
402+
customRecords := packet.CustomRecords
403+
404+
// Add custom records entry.
405+
crKey := uint64(65537)
406+
crValue := []byte("custom-records-test-value")
407+
customRecords[crKey] = crValue
408+
409+
action := routerrpc.ResolveHoldForwardAction_RESUME_MODIFIED
410+
newOutgoingAmountMsat := packet.OutgoingAmountMsat + 4000
411+
412+
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
413+
IncomingCircuitKey: packet.IncomingCircuitKey,
414+
OutgoingAmountMsat: newOutgoingAmountMsat,
415+
CustomRecords: customRecords,
416+
Action: action,
417+
})
418+
require.NoError(ht, err, "failed to send request")
419+
420+
// Check that the modified UpdateAddHTLC message fields were reported in
421+
// Carol's log.
422+
targetLogPrefixStr := "Received UpdateAddHTLC("
423+
targetOutgoingAmountMsatStr := fmt.Sprintf(
424+
"amt=%d", newOutgoingAmountMsat,
425+
)
426+
427+
// Formulate custom records target log string.
428+
var asciiValues []string
429+
for _, b := range crValue {
430+
asciiValues = append(asciiValues, fmt.Sprintf("%d", b))
431+
}
432+
433+
targetCustomRecordsStr := fmt.Sprintf(
434+
"%d:[%s]", crKey, strings.Join(asciiValues, " "),
435+
)
436+
437+
// logEntryCheck is a helper function that checks if the log entry
438+
// contains the expected strings.
439+
logEntryCheck := func(logEntry string) bool {
440+
return strings.Contains(logEntry, targetLogPrefixStr) &&
441+
strings.Contains(logEntry, targetCustomRecordsStr) &&
442+
strings.Contains(logEntry, targetOutgoingAmountMsatStr)
443+
}
444+
445+
// Wait for the log entry to appear in Carol's log.
446+
require.Eventually(ht, func() bool {
447+
ctx := context.Background()
448+
dbgInfo, err := carol.RPC.LN.GetDebugInfo(
449+
ctx, &lnrpc.GetDebugInfoRequest{},
450+
)
451+
require.NoError(ht, err, "failed to get Carol node debug info")
452+
453+
for _, logEntry := range dbgInfo.Log {
454+
if logEntryCheck(logEntry) {
455+
return true
456+
}
457+
}
458+
459+
return false
460+
}, defaultTimeout, time.Second)
461+
462+
// Cancel the context, which will disconnect Bob's interceptor.
463+
cancelBobInterceptor()
464+
465+
// Make sure all goroutines are finished.
466+
select {
467+
case <-done:
468+
case <-time.After(defaultTimeout):
469+
require.Fail(ht, "timeout waiting for sending payment")
470+
}
471+
472+
// Assert that the payment was successful.
473+
var preimage lntypes.Preimage
474+
copy(preimage[:], invoice.RPreimage)
475+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
476+
477+
// Finally, close channels.
478+
ht.CloseChannel(alice, cpAB)
479+
ht.CloseChannel(bob, cpBC)
480+
}
481+
347482
// interceptorTestScenario is a helper struct to hold the test context and
348483
// provide the needed functionality.
349484
type interceptorTestScenario struct {

0 commit comments

Comments
 (0)