Skip to content

Commit 427d41d

Browse files
GeorgeTsagkguggero
authored andcommitted
itest: add interceptor and first hop data tests
1 parent 81c8331 commit 427d41d

File tree

3 files changed

+301
-5
lines changed

3 files changed

+301
-5
lines changed

itest/list_on_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,14 @@ var allTestCases = []*lntest.TestCase{
458458
Name: "forward interceptor modified htlc",
459459
TestFunc: testForwardInterceptorModifiedHtlc,
460460
},
461+
{
462+
Name: "forward interceptor wire records",
463+
TestFunc: testForwardInterceptorWireRecords,
464+
},
465+
{
466+
Name: "forward interceptor restart",
467+
TestFunc: testForwardInterceptorRestart,
468+
},
461469
{
462470
Name: "zero conf channel open",
463471
TestFunc: testZeroConfChannelOpen,

itest/lnd_forward_interceptor_test.go

Lines changed: 283 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package itest
22

33
import (
4+
"bytes"
45
"fmt"
6+
"reflect"
57
"strings"
68
"time"
79

@@ -13,6 +15,7 @@ import (
1315
"github.com/lightningnetwork/lnd/lntest/node"
1416
"github.com/lightningnetwork/lnd/lntest/wait"
1517
"github.com/lightningnetwork/lnd/lntypes"
18+
"github.com/lightningnetwork/lnd/lnwire"
1619
"github.com/lightningnetwork/lnd/routing/route"
1720
"github.com/stretchr/testify/require"
1821
"google.golang.org/grpc/codes"
@@ -24,6 +27,7 @@ var (
2427
customTestValue = []byte{1, 3, 5}
2528

2629
actionResumeModify = routerrpc.ResolveHoldForwardAction_RESUME_MODIFIED
30+
actionResume = routerrpc.ResolveHoldForwardAction_RESUME
2731
)
2832

2933
type interceptorTestCase struct {
@@ -436,33 +440,309 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
436440
ht.CloseChannel(bob, cpBC)
437441
}
438442

443+
// testForwardInterceptorWireRecords tests that the interceptor can read any
444+
// wire custom records provided by the sender of a payment as part of the
445+
// update_add_htlc message.
446+
func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
447+
// Initialize the test context with 3 connected nodes.
448+
ts := newInterceptorTestScenario(ht)
449+
450+
alice, bob, carol, dave := ts.alice, ts.bob, ts.carol, ts.dave
451+
452+
// Open and wait for channels.
453+
const chanAmt = btcutil.Amount(300000)
454+
p := lntest.OpenChannelParams{Amt: chanAmt}
455+
reqs := []*lntest.OpenChannelRequest{
456+
{Local: alice, Remote: bob, Param: p},
457+
{Local: bob, Remote: carol, Param: p},
458+
{Local: carol, Remote: dave, Param: p},
459+
}
460+
resp := ht.OpenMultiChannelsAsync(reqs)
461+
cpAB, cpBC, cpCD := resp[0], resp[1], resp[2]
462+
463+
// Make sure Alice is aware of channel Bob=>Carol.
464+
ht.AssertTopologyChannelOpen(alice, cpBC)
465+
466+
// Connect an interceptor to Bob's node.
467+
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
468+
defer cancelBobInterceptor()
469+
470+
// Also connect an interceptor on Carol's node to check whether we're
471+
// relaying the TLVs send in update_add_htlc over Alice -> Bob on the
472+
// Bob -> Carol link.
473+
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
474+
defer cancelCarolInterceptor()
475+
476+
req := &lnrpc.Invoice{ValueMsat: 1000}
477+
addResponse := dave.RPC.AddInvoice(req)
478+
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
479+
480+
sendReq := &routerrpc.SendPaymentRequest{
481+
PaymentRequest: invoice.PaymentRequest,
482+
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
483+
FeeLimitMsat: noFeeLimitMsat,
484+
FirstHopCustomRecords: map[uint64][]byte{
485+
65537: []byte("test"),
486+
},
487+
}
488+
489+
_ = alice.RPC.SendPayment(sendReq)
490+
491+
// We start the htlc interceptor with a simple implementation that saves
492+
// all intercepted packets. These packets are held to simulate a
493+
// pending payment.
494+
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
495+
496+
require.Len(ht, packet.InWireCustomRecords, 1)
497+
498+
val, ok := packet.InWireCustomRecords[65537]
499+
require.True(ht, ok, "expected custom record")
500+
require.Equal(ht, []byte("test"), val)
501+
502+
// TODO(guggero): Actually modify the amount once we have the invoice
503+
// interceptor and can accept a lower amount.
504+
newOutAmountMsat := packet.OutgoingAmountMsat
505+
506+
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
507+
IncomingCircuitKey: packet.IncomingCircuitKey,
508+
OutAmountMsat: newOutAmountMsat,
509+
Action: actionResumeModify,
510+
})
511+
require.NoError(ht, err, "failed to send request")
512+
513+
// Assert that the Alice -> Bob custom records in update_add_htlc are
514+
// not propagated on the Bob -> Carol link.
515+
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
516+
require.Len(ht, packet.InWireCustomRecords, 0)
517+
518+
// Just resume the payment on Carol.
519+
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
520+
IncomingCircuitKey: packet.IncomingCircuitKey,
521+
Action: actionResume,
522+
})
523+
require.NoError(ht, err, "carol interceptor response")
524+
525+
// Assert that the payment was successful.
526+
var preimage lntypes.Preimage
527+
copy(preimage[:], invoice.RPreimage)
528+
ht.AssertPaymentStatus(
529+
alice, preimage, lnrpc.Payment_SUCCEEDED,
530+
func(p *lnrpc.Payment) error {
531+
recordsEqual := reflect.DeepEqual(
532+
p.FirstHopCustomRecords,
533+
sendReq.FirstHopCustomRecords,
534+
)
535+
if !recordsEqual {
536+
return fmt.Errorf("expected custom records to "+
537+
"be equal, got %v expected %v",
538+
p.FirstHopCustomRecords,
539+
sendReq.FirstHopCustomRecords)
540+
}
541+
542+
return nil
543+
},
544+
)
545+
546+
// Finally, close channels.
547+
ht.CloseChannel(alice, cpAB)
548+
ht.CloseChannel(bob, cpBC)
549+
ht.CloseChannel(carol, cpCD)
550+
}
551+
552+
// testForwardInterceptorRestart tests that the interceptor can read any wire
553+
// custom records provided by the sender of a payment as part of the
554+
// update_add_htlc message and that those records are persisted correctly and
555+
// re-sent on node restart.
556+
func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
557+
// Initialize the test context with 3 connected nodes.
558+
ts := newInterceptorTestScenario(ht)
559+
560+
alice, bob, carol, dave := ts.alice, ts.bob, ts.carol, ts.dave
561+
562+
// Open and wait for channels.
563+
const chanAmt = btcutil.Amount(300000)
564+
p := lntest.OpenChannelParams{Amt: chanAmt}
565+
reqs := []*lntest.OpenChannelRequest{
566+
{Local: alice, Remote: bob, Param: p},
567+
{Local: bob, Remote: carol, Param: p},
568+
{Local: carol, Remote: dave, Param: p},
569+
}
570+
resp := ht.OpenMultiChannelsAsync(reqs)
571+
cpAB, cpBC, cpCD := resp[0], resp[1], resp[2]
572+
573+
// Make sure Alice is aware of channels Bob=>Carol and Carol=>Dave.
574+
ht.AssertTopologyChannelOpen(alice, cpBC)
575+
ht.AssertTopologyChannelOpen(alice, cpCD)
576+
577+
// Connect an interceptor to Bob's node.
578+
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
579+
580+
// Also connect an interceptor on Carol's node to check whether we're
581+
// relaying the TLVs send in update_add_htlc over Alice -> Bob on the
582+
// Bob -> Carol link.
583+
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
584+
defer cancelCarolInterceptor()
585+
586+
req := &lnrpc.Invoice{ValueMsat: 50_000_000}
587+
addResponse := dave.RPC.AddInvoice(req)
588+
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
589+
590+
customRecords := map[uint64][]byte{
591+
65537: []byte("test"),
592+
}
593+
594+
sendReq := &routerrpc.SendPaymentRequest{
595+
PaymentRequest: invoice.PaymentRequest,
596+
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
597+
FeeLimitMsat: noFeeLimitMsat,
598+
FirstHopCustomRecords: customRecords,
599+
}
600+
601+
_ = alice.RPC.SendPayment(sendReq)
602+
603+
// We start the htlc interceptor with a simple implementation that saves
604+
// all intercepted packets. These packets are held to simulate a
605+
// pending payment.
606+
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
607+
608+
require.Len(ht, packet.InWireCustomRecords, 1)
609+
require.Equal(ht, customRecords, packet.InWireCustomRecords)
610+
611+
// We accept the payment at Bob and resume it, so it gets to Carol.
612+
// This means the HTLC should now be fully locked in on Alice's side and
613+
// any restart of the node should cause the payment to be resumed and
614+
// the data to be persisted across restarts.
615+
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
616+
IncomingCircuitKey: packet.IncomingCircuitKey,
617+
Action: actionResume,
618+
})
619+
require.NoError(ht, err, "failed to send request")
620+
621+
// We don't resume the payment on Carol, so it should be held there.
622+
623+
// The payment should now be in flight.
624+
var preimage lntypes.Preimage
625+
copy(preimage[:], invoice.RPreimage)
626+
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
627+
628+
// We don't resume the payment on Carol, so it should be held there.
629+
// We now restart first Bob, then Alice, so we can make sure we've
630+
// started the interceptor again on Bob before Alice resumes the
631+
// payment.
632+
cancelBobInterceptor()
633+
restartBob := ht.SuspendNode(bob)
634+
restartAlice := ht.SuspendNode(alice)
635+
636+
require.NoError(ht, restartBob(), "failed to restart bob")
637+
bobInterceptor, cancelBobInterceptor = bob.RPC.HtlcInterceptor()
638+
defer cancelBobInterceptor()
639+
640+
require.NoError(ht, restartAlice(), "failed to restart alice")
641+
642+
// We should get another notification about the held HTLC.
643+
packet = ht.ReceiveHtlcInterceptor(bobInterceptor)
644+
645+
require.Len(ht, packet.InWireCustomRecords, 1)
646+
require.Equal(ht, customRecords, packet.InWireCustomRecords)
647+
648+
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
649+
IncomingCircuitKey: packet.IncomingCircuitKey,
650+
Action: actionResume,
651+
})
652+
require.NoError(ht, err, "failed to send request")
653+
654+
// And now we forward the payment at Carol.
655+
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
656+
require.Len(ht, packet.InWireCustomRecords, 0)
657+
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
658+
IncomingCircuitKey: packet.IncomingCircuitKey,
659+
Action: actionResume,
660+
})
661+
require.NoError(ht, err, "failed to send request")
662+
663+
// Assert that the payment was successful.
664+
ht.AssertPaymentStatus(
665+
alice, preimage, lnrpc.Payment_SUCCEEDED,
666+
func(p *lnrpc.Payment) error {
667+
recordsEqual := reflect.DeepEqual(
668+
p.FirstHopCustomRecords,
669+
sendReq.FirstHopCustomRecords,
670+
)
671+
if !recordsEqual {
672+
return fmt.Errorf("expected custom records to "+
673+
"be equal, got %v expected %v",
674+
p.FirstHopCustomRecords,
675+
sendReq.FirstHopCustomRecords)
676+
}
677+
678+
if len(p.Htlcs) != 1 {
679+
return fmt.Errorf("expected 1 htlc, got %d",
680+
len(p.Htlcs))
681+
}
682+
683+
htlc := p.Htlcs[0]
684+
rt := htlc.Route
685+
if rt.FirstHopAmountMsat != rt.TotalAmtMsat {
686+
return fmt.Errorf("expected first hop amount "+
687+
"to be %d, got %d", rt.TotalAmtMsat,
688+
rt.FirstHopAmountMsat)
689+
}
690+
691+
cr := lnwire.CustomRecords(p.FirstHopCustomRecords)
692+
recordData, err := cr.Serialize()
693+
if err != nil {
694+
return err
695+
}
696+
697+
if !bytes.Equal(rt.CustomChannelData, recordData) {
698+
return fmt.Errorf("expected custom records to "+
699+
"be equal, got %x expected %x",
700+
rt.CustomChannelData, recordData)
701+
}
702+
703+
return nil
704+
},
705+
)
706+
707+
// Finally, close channels.
708+
ht.CloseChannel(alice, cpAB)
709+
ht.CloseChannel(bob, cpBC)
710+
ht.CloseChannel(carol, cpCD)
711+
}
712+
439713
// interceptorTestScenario is a helper struct to hold the test context and
440714
// provide the needed functionality.
441715
type interceptorTestScenario struct {
442-
ht *lntest.HarnessTest
443-
alice, bob, carol *node.HarnessNode
716+
ht *lntest.HarnessTest
717+
alice, bob, carol, dave *node.HarnessNode
444718
}
445719

446720
// newInterceptorTestScenario initializes a new test scenario with three nodes
447721
// and connects them to have the following topology,
448722
//
449-
// Alice --> Bob --> Carol
723+
// Alice --> Bob --> Carol --> Dave
450724
//
451725
// Among them, Alice and Bob are standby nodes and Carol is a new node.
452726
func newInterceptorTestScenario(
453727
ht *lntest.HarnessTest) *interceptorTestScenario {
454728

455729
alice, bob := ht.Alice, ht.Bob
456730
carol := ht.NewNode("carol", nil)
731+
dave := ht.NewNode("dave", nil)
457732

458733
ht.EnsureConnected(alice, bob)
459734
ht.EnsureConnected(bob, carol)
735+
ht.EnsureConnected(carol, dave)
736+
737+
// So that carol can open channels.
738+
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
460739

461740
return &interceptorTestScenario{
462741
ht: ht,
463742
alice: alice,
464743
bob: bob,
465744
carol: carol,
745+
dave: dave,
466746
}
467747
}
468748

lntest/harness_assertion.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,13 +1600,16 @@ func (h *HarnessTest) findPayment(hn *node.HarnessNode,
16001600
return nil
16011601
}
16021602

1603+
// PaymentCheck is a function that checks a payment for a specific condition.
1604+
type PaymentCheck func(*lnrpc.Payment) error
1605+
16031606
// AssertPaymentStatus asserts that the given node list a payment with the
16041607
// given preimage has the expected status. It also checks that the payment has
16051608
// the expected preimage, which is empty when it's not settled and matches the
16061609
// given preimage when it's succeeded.
16071610
func (h *HarnessTest) AssertPaymentStatus(hn *node.HarnessNode,
1608-
preimage lntypes.Preimage,
1609-
status lnrpc.Payment_PaymentStatus) *lnrpc.Payment {
1611+
preimage lntypes.Preimage, status lnrpc.Payment_PaymentStatus,
1612+
checks ...PaymentCheck) *lnrpc.Payment {
16101613

16111614
var target *lnrpc.Payment
16121615
payHash := preimage.Hash()
@@ -1636,6 +1639,11 @@ func (h *HarnessTest) AssertPaymentStatus(hn *node.HarnessNode,
16361639
target.PaymentPreimage, "expected zero preimage")
16371640
}
16381641

1642+
// Perform any additional checks on the payment.
1643+
for _, check := range checks {
1644+
require.NoError(h, check(target))
1645+
}
1646+
16391647
return target
16401648
}
16411649

0 commit comments

Comments
 (0)