From 6f4034f7d1100615e13229964fdb7716dea25395 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 6 Dec 2022 13:52:00 +0200 Subject: [PATCH] lntest/itest: add session deletion itest --- itest/list_on_test.go | 4 + itest/lnd_watchtower_test.go | 172 +++++++++++++++++++++++++++++++++++ lntest/rpc/watchtower.go | 14 +++ 3 files changed, 190 insertions(+) create mode 100644 itest/lnd_watchtower_test.go diff --git a/itest/list_on_test.go b/itest/list_on_test.go index d2253969f..19f083cbb 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -515,4 +515,8 @@ var allTestCases = []*lntest.TestCase{ Name: "lookup htlc resolution", TestFunc: testLookupHtlcResolution, }, + { + Name: "watchtower session management", + TestFunc: testWatchtowerSessionManagement, + }, } diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go new file mode 100644 index 000000000..3432848d8 --- /dev/null +++ b/itest/lnd_watchtower_test.go @@ -0,0 +1,172 @@ +package itest + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/funding" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/stretchr/testify/require" +) + +// testWatchtowerSessionManagement tests that session deletion is done +// correctly. +func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { + const ( + chanAmt = funding.MaxBtcFundingAmount + paymentAmt = 10_000 + numInvoices = 5 + maxUpdates = numInvoices * 2 + externalIP = "1.2.3.4" + sessionCloseRange = 1 + ) + + // Set up Wallis the watchtower who will be used by Dave to watch over + // his channel commitment transactions. + wallis := ht.NewNode("Wallis", []string{ + "--watchtower.active", + "--watchtower.externalip=" + externalIP, + }) + + wallisInfo := wallis.RPC.GetInfoWatchtower() + + // Assert that Wallis has one listener and it is 0.0.0.0:9911 or + // [::]:9911. Since no listener is explicitly specified, one of these + // should be the default depending on whether the host supports IPv6 or + // not. + require.Len(ht, wallisInfo.Listeners, 1) + listener := wallisInfo.Listeners[0] + require.True(ht, listener == "0.0.0.0:9911" || listener == "[::]:9911") + + // Assert the Wallis's URIs properly display the chosen external IP. + require.Len(ht, wallisInfo.Uris, 1) + require.Contains(ht, wallisInfo.Uris[0], externalIP) + + // Dave will be the tower client. + daveArgs := []string{ + "--wtclient.active", + fmt.Sprintf("--wtclient.max-updates=%d", maxUpdates), + fmt.Sprintf( + "--wtclient.session-close-range=%d", sessionCloseRange, + ), + } + dave := ht.NewNode("Dave", daveArgs) + + addTowerReq := &wtclientrpc.AddTowerRequest{ + Pubkey: wallisInfo.Pubkey, + Address: listener, + } + dave.RPC.AddTower(addTowerReq) + + // Assert that there exists a session between Dave and Wallis. + err := wait.NoError(func() error { + info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{ + Pubkey: wallisInfo.Pubkey, + IncludeSessions: true, + }) + + var numSessions uint32 + for _, sessionType := range info.SessionInfo { + numSessions += sessionType.NumSessions + } + if numSessions > 0 { + return nil + } + + return fmt.Errorf("expected a non-zero number of sessions") + }, defaultTimeout) + require.NoError(ht, err) + + // Before we make a channel, we'll load up Dave with some coins sent + // directly from the miner. + ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) + + // Connect Dave and Alice. + ht.ConnectNodes(dave, ht.Alice) + + // Open a channel between Dave and Alice. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + } + chanPoint := ht.OpenChannel(dave, ht.Alice, params) + + // Since there are 2 updates made for every payment and the maximum + // number of updates per session has been set to 10, make 5 payments + // between the pair so that the session is exhausted. + alicePayReqs, _, _ := ht.CreatePayReqs( + ht.Alice, paymentAmt, numInvoices, + ) + + send := func(node *node.HarnessNode, payReq string) { + stream := node.RPC.SendPayment(&routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }) + + ht.AssertPaymentStatusFromStream( + stream, lnrpc.Payment_SUCCEEDED, + ) + } + + for i := 0; i < numInvoices; i++ { + send(dave, alicePayReqs[i]) + } + + // assertNumBackups is a closure that asserts that Dave has a certain + // number of backups backed up to the tower. If mineOnFail is true, + // then a block will be mined each time the assertion fails. + assertNumBackups := func(expected int, mineOnFail bool) { + err = wait.NoError(func() error { + info := dave.RPC.GetTowerInfo( + &wtclientrpc.GetTowerInfoRequest{ + Pubkey: wallisInfo.Pubkey, + IncludeSessions: true, + }, + ) + + var numBackups uint32 + for _, sessionType := range info.SessionInfo { + for _, session := range sessionType.Sessions { + numBackups += session.NumBackups + } + } + + if numBackups == uint32(expected) { + return nil + } + + if mineOnFail { + ht.Miner.MineBlocksSlow(1) + } + + return fmt.Errorf("expected %d backups, got %d", + expected, numBackups) + }, defaultTimeout) + require.NoError(ht, err) + } + + // Assert that one of the sessions now has 10 backups. + assertNumBackups(10, false) + + // Now close the channel and wait for the close transaction to appear + // in the mempool so that it is included in a block when we mine. + ht.CloseChannelAssertPending(dave, chanPoint, false) + + // Mine enough blocks to surpass the session-close-range. This should + // trigger the session to be deleted. + ht.MineBlocksAndAssertNumTxes(sessionCloseRange+6, 1) + + // Wait for the session to be deleted. We know it has been deleted once + // the number of backups is back to zero. We check for number of backups + // instead of number of sessions because it is expected that the client + // would immediately negotiate another session after deleting the + // exhausted one. This time we set the "mineOnFail" parameter to true to + // ensure that the session deleting logic is run. + assertNumBackups(0, true) +} diff --git a/lntest/rpc/watchtower.go b/lntest/rpc/watchtower.go index 1b05d15ea..d5512e7a4 100644 --- a/lntest/rpc/watchtower.go +++ b/lntest/rpc/watchtower.go @@ -24,6 +24,20 @@ func (h *HarnessRPC) GetInfoWatchtower() *watchtowerrpc.GetInfoResponse { return info } +// GetTowerInfo makes an RPC call to the watchtower client of the given node and +// asserts. +func (h *HarnessRPC) GetTowerInfo( + req *wtclientrpc.GetTowerInfoRequest) *wtclientrpc.Tower { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + info, err := h.WatchtowerClient.GetTowerInfo(ctxt, req) + h.NoError(err, "GetTowerInfo from WatchtowerClient") + + return info +} + // AddTower makes a RPC call to the WatchtowerClient of the given node and // asserts. func (h *HarnessRPC) AddTower(