From 08ca28773c9e5a6a760c61b34923d37a84307337 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Fri, 8 Mar 2024 13:21:22 +0100 Subject: [PATCH] itest: add dynamic scid alias routing test --- itest/list_on_test.go | 4 + itest/lnd_routing_test.go | 189 ++++++++++++++++++++++++++++++++++++++ lntest/rpc/harness_rpc.go | 3 + lntest/rpc/router.go | 48 ++++++++++ 4 files changed, 244 insertions(+) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 82fcdd99b..147a0de0c 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -358,6 +358,10 @@ var allTestCases = []*lntest.TestCase{ Name: "invoice routing hints", TestFunc: testInvoiceRoutingHints, }, + { + Name: "scid alias routing hints", + TestFunc: testScidAliasRoutingHints, + }, { Name: "multi-hop payments over private channels", TestFunc: testMultiHopOverPrivateChannels, diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go index 0b1cfebe8..d92de5aba 100644 --- a/itest/lnd_routing_test.go +++ b/itest/lnd_routing_test.go @@ -3,6 +3,7 @@ package itest import ( "encoding/hex" "fmt" + "math" "testing" "time" @@ -746,6 +747,194 @@ func testInvoiceRoutingHints(ht *lntest.HarnessTest) { ht.ForceCloseChannel(alice, chanPointEve) } +// testScidAliasRoutingHints tests that dynamically created aliases via the RPC +// are properly used when routing. +func testScidAliasRoutingHints(ht *lntest.HarnessTest) { + const chanAmt = btcutil.Amount(800000) + + // Option-scid-alias is opt-in, as is anchors. + scidAliasArgs := []string{ + "--protocol.option-scid-alias", + "--protocol.anchors", + } + + // We'll have a network Bob -> Carol -> Dave in the end, with both Carol + // and Dave having an alias for the channel between them. + carol := ht.NewNode("Carol", scidAliasArgs) + dave := ht.NewNode("Dave", scidAliasArgs) + + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) + + ht.ConnectNodes(carol, dave) + + // Create a channel between Carol and Dave, which uses the scid alias + // feature. + chanPointCD := ht.OpenChannel(carol, dave, lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: chanAmt / 2, + ScidAlias: true, + Private: true, + }) + + // Find the channel ID of the channel between Carol and Dave. + carolDaveChan := ht.QueryChannelByChanPoint(carol, chanPointCD) + + // Make sure we can't add an alias that's not actually in the alias + // range (which is the case with the base SCID). + err := carol.RPC.XAddLocalChanAliasesErr(&routerrpc.AddAliasesRequest{ + AliasMaps: []*lnrpc.AliasMap{ + { + BaseScid: carolDaveChan.ChanId, + Aliases: []uint64{ + carolDaveChan.ChanId, + }, + }, + }, + }) + require.ErrorContains(ht, err, routerrpc.ErrNoValidAlias.Error()) + + // Create an ephemeral alias that will be used as a routing hint. + ephemeralChanPoint := lnwire.ShortChannelID{ + BlockHeight: 16_100_000, + TxIndex: 1, + TxPosition: 1, + } + ephemeralAlias := ephemeralChanPoint.ToUint64() + ephemeralAliasMap := []*lnrpc.AliasMap{ + { + BaseScid: carolDaveChan.ChanId, + Aliases: []uint64{ + ephemeralAlias, + }, + }, + } + + // Add the alias to Carol. + carol.RPC.XAddLocalChanAliases(&routerrpc.AddAliasesRequest{ + AliasMaps: ephemeralAliasMap, + }) + + // We shouldn't be able to add the same alias again. + err = carol.RPC.XAddLocalChanAliasesErr(&routerrpc.AddAliasesRequest{ + AliasMaps: ephemeralAliasMap, + }) + require.ErrorContains(ht, err, routerrpc.ErrAliasAlreadyExists.Error()) + + // Add the alias to Dave. This isn't strictly needed for the test, as + // the payment will go from Bob -> Carol -> Dave, so only Carol needs + // to know about the alias. So we'll later on remove it again to + // demonstrate that. + dave.RPC.XAddLocalChanAliases(&routerrpc.AddAliasesRequest{ + AliasMaps: ephemeralAliasMap, + }) + + carolChans := carol.RPC.ListChannels(&lnrpc.ListChannelsRequest{}) + require.Len(ht, carolChans.Channels, 1, "expected one channel") + + // Get the alias scids for Carol's channel. + aliases := carolChans.Channels[0].AliasScids + + // There should be two aliases. + require.Len(ht, aliases, 2, "expected two aliases") + + // The ephemeral alias should be included. + require.Contains( + ht, aliases, ephemeralAlias, "expected ephemeral alias", + ) + + // List Dave's Channels. + daveChans := dave.RPC.ListChannels(&lnrpc.ListChannelsRequest{}) + + require.Len(ht, daveChans.Channels, 1, "expected one channel") + + // Get the alias scids for his channel. + aliases = daveChans.Channels[0].AliasScids + + // There should be two aliases. + require.Len(ht, aliases, 2, "expected two aliases") + + // The ephemeral alias should be included. + require.Contains( + ht, aliases, ephemeralAlias, "expected ephemeral alias", + ) + + // Now that we've asserted that the alias is properly set up, we'll + // delete the one for Dave again. The payment should still succeed. + dave.RPC.XDeleteLocalChanAliases(&routerrpc.DeleteAliasesRequest{ + AliasMaps: ephemeralAliasMap, + }) + + // Connect the existing Bob node with Carol via a public channel. + ht.ConnectNodes(ht.Bob, carol) + chanPointBC := ht.OpenChannel(ht.Bob, carol, lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: chanAmt / 2, + }) + + // Create the hop hint that Dave will use to craft his invoice. The + // goal here is to define only the ephemeral alias as a hop hint. + hopHint := &lnrpc.HopHint{ + NodeId: carol.PubKeyStr, + ChanId: ephemeralAlias, + FeeBaseMsat: uint32( + chainreg.DefaultBitcoinBaseFeeMSat, + ), + FeeProportionalMillionths: uint32( + chainreg.DefaultBitcoinFeeRate, + ), + CltvExpiryDelta: chainreg.DefaultBitcoinTimeLockDelta, + } + + // Define the invoice that Dave will add to his node. + invoice := &lnrpc.Invoice{ + Memo: "dynamic alias", + Value: int64(chanAmt / 4), + RouteHints: []*lnrpc.RouteHint{ + { + HopHints: []*lnrpc.HopHint{hopHint}, + }, + }, + } + + // Add the invoice and retrieve the payment request. + payReq := dave.RPC.AddInvoice(invoice).PaymentRequest + + // Now Alice will try to pay to that payment request. + timeout := time.Second * 15 + stream := ht.Bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: int32(timeout.Seconds()), + FeeLimitSat: math.MaxInt64, + }) + + // Payment should eventually succeed. + ht.AssertPaymentSucceedWithTimeout(stream, timeout) + + // Check that Dave's invoice appears as settled. + invoices := dave.RPC.ListInvoices(&lnrpc.ListInvoiceRequest{}) + require.Len(ht, invoices.Invoices, 1, "expected one invoice") + require.Equal(ht, invoices.Invoices[0].State, lnrpc.Invoice_SETTLED, + "expected settled invoice") + + // We'll now delete the alias again, but only on Carol's end. That + // should be enough to make the payment fail, since she doesn't know + // about the alias in the hop hint anymore. + carol.RPC.XDeleteLocalChanAliases(&routerrpc.DeleteAliasesRequest{ + AliasMaps: ephemeralAliasMap, + }) + payReq2 := dave.RPC.AddInvoice(invoice).PaymentRequest + stream2 := ht.Bob.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + PaymentRequest: payReq2, + TimeoutSeconds: int32(timeout.Seconds()), + FeeLimitSat: math.MaxInt64, + }) + ht.AssertPaymentStatusFromStream(stream2, lnrpc.Payment_FAILED) + + ht.CloseChannel(carol, chanPointCD) + ht.CloseChannel(ht.Bob, chanPointBC) +} + // testMultiHopOverPrivateChannels tests that private channels can be used as // intermediate hops in a route for payments. func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) { diff --git a/lntest/rpc/harness_rpc.go b/lntest/rpc/harness_rpc.go index 0e8256ce1..0640581dc 100644 --- a/lntest/rpc/harness_rpc.go +++ b/lntest/rpc/harness_rpc.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/chainrpc" + "github.com/lightningnetwork/lnd/lnrpc/devrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/neutrinorpc" "github.com/lightningnetwork/lnd/lnrpc/peersrpc" @@ -42,6 +43,7 @@ type HarnessRPC struct { ChainKit chainrpc.ChainKitClient NeutrinoKit neutrinorpc.NeutrinoKitClient Peer peersrpc.PeersClient + DevRPC devrpc.DevClient // Name is the HarnessNode's name. Name string @@ -73,6 +75,7 @@ func NewHarnessRPC(ctxt context.Context, t *testing.T, c *grpc.ClientConn, ChainKit: chainrpc.NewChainKitClient(c), NeutrinoKit: neutrinorpc.NewNeutrinoKitClient(c), Peer: peersrpc.NewPeersClient(c), + DevRPC: devrpc.NewDevClient(c), Name: name, } diff --git a/lntest/rpc/router.go b/lntest/rpc/router.go index 8bb8a92e3..7c3f2e7c4 100644 --- a/lntest/rpc/router.go +++ b/lntest/rpc/router.go @@ -208,6 +208,54 @@ func (h *HarnessRPC) HtlcInterceptor() (InterceptorClient, context.CancelFunc) { return resp, cancel } +// XAddLocalChanAliases adds a list of aliases to the node's alias map. +func (h *HarnessRPC) XAddLocalChanAliases(req *routerrpc.AddAliasesRequest) { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XAddLocalChanAliases(ctxt, req) + h.NoError(err, "XAddLocalChanAliases") +} + +// XAddLocalChanAliasesErr adds a list of aliases to the node's alias map and +// expects an error. +func (h *HarnessRPC) XAddLocalChanAliasesErr( + req *routerrpc.AddAliasesRequest) error { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XAddLocalChanAliases(ctxt, req) + require.Error(h, err) + + return err +} + +// XDeleteLocalChanAliases deleted a set of alias mappings. +func (h *HarnessRPC) XDeleteLocalChanAliases( + req *routerrpc.DeleteAliasesRequest) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XDeleteLocalChanAliases(ctxt, req) + h.NoError(err, "XDeleteLocalChanAliases") +} + +// XDeleteLocalChanAliasesErr deleted a set of alias mappings and expects an +// error. +func (h *HarnessRPC) XDeleteLocalChanAliasesErr( + req *routerrpc.DeleteAliasesRequest) error { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XDeleteLocalChanAliases(ctxt, req) + require.Error(h, err) + + return err +} + type TrackPaymentsClient routerrpc.Router_TrackPaymentsClient // TrackPayments makes a RPC call to the node's RouterClient and asserts.