package itest import ( "fmt" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/go-errors/errors" "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/port" "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) // testWatchtower tests the behaviour of the watchtower client and server. func testWatchtower(ht *lntest.HarnessTest) { ht.Run("revocation", func(t *testing.T) { tt := ht.Subtest(t) testRevokedCloseRetributionAltruistWatchtower(tt) }) ht.Run("session deletion", func(t *testing.T) { tt := ht.Subtest(t) testTowerClientSessionDeletion(tt) }) ht.Run("tower and session activation", func(t *testing.T) { tt := ht.Subtest(t) testTowerClientTowerAndSessionManagement(tt) }) } // testTowerClientTowerAndSessionManagement tests the various control commands // that a user has over the client's set of active towers and sessions. func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount externalIP = "1.2.3.4" externalIP2 = "1.2.3.5" sessionCloseRange = 1 ) // Set up Wallis the watchtower who will be used by Dave to watch over // his channel commitment transactions. wallisPk, wallisListener, _ := setUpNewTower(ht, "Wallis", externalIP) // Dave will be the tower client. daveArgs := []string{ "--wtclient.active", fmt.Sprintf( "--wtclient.session-close-range=%d", sessionCloseRange, ), } dave := ht.NewNode("Dave", daveArgs) addWallisReq := &wtclientrpc.AddTowerRequest{ Pubkey: wallisPk, Address: wallisListener, } dave.RPC.AddTower(addWallisReq) assertNumSessions := func(towerPk []byte, expectedNum int, mineOnFail bool) { err := wait.NoError(func() error { info := dave.RPC.GetTowerInfo( &wtclientrpc.GetTowerInfoRequest{ Pubkey: towerPk, IncludeSessions: true, }, ) var numSessions uint32 for _, sessionType := range info.SessionInfo { numSessions += sessionType.NumSessions } if numSessions == uint32(expectedNum) { return nil } if mineOnFail { ht.MineBlocks(1) } return fmt.Errorf("expected %d sessions, got %d", expectedNum, numSessions) }, defaultTimeout) require.NoError(ht, err) } // Assert that there are a few sessions between Dave and Wallis. There // should be one per client. There are currently 3 types of clients, so // we expect 3 sessions. assertNumSessions(wallisPk, 3, false) // 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) // Show that the Wallis tower is currently seen as an active session // candidate. info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{ Pubkey: wallisPk, }) require.GreaterOrEqual(ht, len(info.SessionInfo), 1) require.True(ht, info.SessionInfo[0].ActiveSessionCandidate) // Make some back-ups and assert that they are added to a session with // the tower. generateBackups(ht, dave, ht.Alice, 4) // Assert that one of the sessions now has 4 backups. assertNumBackups(ht, dave.RPC, wallisPk, 4, false) // Now, deactivate the tower and show that it is no longer considered // an active session candidate. dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{ Pubkey: wallisPk, }) info = dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{ Pubkey: wallisPk, }) require.GreaterOrEqual(ht, len(info.SessionInfo), 1) require.False(ht, info.SessionInfo[0].ActiveSessionCandidate) // Back up a few more states. generateBackups(ht, dave, ht.Alice, 4) // These should _not_ be on the tower. Therefore, the number of // back-ups on the tower should be the same as before. assertNumBackups(ht, dave.RPC, wallisPk, 4, false) // Add new tower and connect Dave to it. wilmaPk, wilmaListener, _ := setUpNewTower(ht, "Wilma", externalIP2) dave.RPC.AddTower(&wtclientrpc.AddTowerRequest{ Pubkey: wilmaPk, Address: wilmaListener, }) assertNumSessions(wilmaPk, 3, false) // The updates from before should now appear on the new watchtower. assertNumBackups(ht, dave.RPC, wilmaPk, 4, false) // Reactivate the Wallis tower and then deactivate the Wilma one. dave.RPC.AddTower(addWallisReq) dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{ Pubkey: wilmaPk, }) // Generate some more back-ups. generateBackups(ht, dave, ht.Alice, 4) // Assert that they get added to the first tower (Wallis) and that the // number of sessions with Wallis has not changed - in other words, the // previously used session was re-used. assertNumBackups(ht, dave.RPC, wallisPk, 8, false) assertNumSessions(wallisPk, 3, false) findSession := func(towerPk []byte, numBackups uint32) []byte { info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{ Pubkey: towerPk, IncludeSessions: true, }) for _, sessionType := range info.SessionInfo { for _, session := range sessionType.Sessions { if session.NumBackups == numBackups { return session.Id } } } ht.Fatalf("session with %d backups not found", numBackups) return nil } // Now we will test the termination of a session. // First, we need to figure out the ID of the session that has been used // for back-ups. sessionID := findSession(wallisPk, 8) // Now, terminate the session. dave.RPC.TerminateSession(&wtclientrpc.TerminateSessionRequest{ SessionId: sessionID, }) // This should force the client to negotiate a new session. The old // session still remains in our session list since the channel for which // it has updates for is still open. assertNumSessions(wallisPk, 4, false) // Any new back-ups should now be backed up on a different session. generateBackups(ht, dave, ht.Alice, 2) assertNumBackups(ht, dave.RPC, wallisPk, 10, false) findSession(wallisPk, 2) // Close the channel. ht.CloseChannelAssertPending(dave, chanPoint, false) // Mine enough blocks to surpass the session close range buffer. ht.MineBlocksAndAssertNumTxes(sessionCloseRange+6, 1) // The session that was previously terminated now gets deleted since // the channel for which it has updates has now been closed. All the // remaining sessions are not yet closable since they are not yet // exhausted and are all still active. We set mineOnFail to true here // so that we can ensure that the closable session queue continues to // be checked on each new block. It could have been the case that all // checks with the above mined blocks were completed before the // closable session was queued. assertNumSessions(wallisPk, 3, true) // For the sake of completion, we call RemoveTower here for both towers // to show that this should never error. dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{ Pubkey: wallisPk, }) dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{ Pubkey: wilmaPk, }) } // testTowerClientSessionDeletion tests that sessions are correctly deleted // when they are deemed closable. func testTowerClientSessionDeletion(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount 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. wallisPk, listener, _ := setUpNewTower(ht, "Wallis", 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: wallisPk, 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: wallisPk, 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. generateBackups(ht, dave, ht.Alice, maxUpdates) // Assert that one of the sessions now has 10 backups. assertNumBackups(ht, dave.RPC, wallisPk, 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(ht, dave.RPC, wallisPk, 0, true) } // testRevokedCloseRetributionAltruistWatchtower establishes a channel between // Carol and Dave, where Carol is using a third node Willy as her watchtower. // After sending some payments, Dave reverts his state and force closes to // trigger a breach. Carol is kept offline throughout the process and the test // asserts that Willy responds by broadcasting the justice transaction on // Carol's behalf sweeping her funds without a reward. func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) { for _, commitType := range []lnrpc.CommitmentType{ lnrpc.CommitmentType_LEGACY, lnrpc.CommitmentType_ANCHORS, lnrpc.CommitmentType_SIMPLE_TAPROOT, } { testName := fmt.Sprintf("%v", commitType.String()) ct := commitType testFunc := func(ht *lntest.HarnessTest) { testRevokedCloseRetributionAltruistWatchtowerCase( ht, ct, ) } success := ht.Run(testName, func(tt *testing.T) { st := ht.Subtest(tt) st.RunTestCase(&lntest.TestCase{ Name: testName, TestFunc: testFunc, }) }) if !success { // Log failure time to help relate the lnd logs to the // failure. ht.Logf("Failure time: %v", time.Now().Format( "2006-01-02 15:04:05.000", )) break } } } func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, commitType lnrpc.CommitmentType) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 numInvoices = 6 externalIP = "1.2.3.4" ) // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. carolArgs := lntest.NodeArgsForCommitType(commitType) carolArgs = append(carolArgs, "--hodl.exit-settle") carol := ht.NewNode("Carol", carolArgs) // Set up Willy the watchtower who will protect Dave from Carol's // breach. He will remain online in order to punish Carol on Dave's // behalf, since the breach will happen while Dave is offline. willyInfoPk, listener, willy := setUpNewTower(ht, "Willy", externalIP) // Dave will be the breached party. We set --nolisten to ensure Carol // won't be able to connect to him and trigger the channel data // protection logic automatically. daveArgs := lntest.NodeArgsForCommitType(commitType) daveArgs = append(daveArgs, "--nolisten", "--wtclient.active") dave := ht.NewNode("Dave", daveArgs) addTowerReq := &wtclientrpc.AddTowerRequest{ Pubkey: willyInfoPk, Address: listener, } dave.RPC.AddTower(addTowerReq) // We must let Dave have an open channel before she can send a node // announcement, so we open a channel with Carol, ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // Send one more UTXOs if this is a neutrino backend. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) } // In order to test Dave's response to an uncooperative channel // closure by Carol, we'll first open up a channel between them with a // 0.5 BTC value. params := lntest.OpenChannelParams{ Amt: 3 * (chanAmt / 4), PushAmt: chanAmt / 4, CommitmentType: commitType, Private: true, } chanPoint := ht.OpenChannel(dave, carol, params) // With the channel open, we'll create a few invoices for Carol that // Dave will pay to in order to advance the state of the channel. carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices) // Next query for Carol's channel state, as we sent 0 payments, Carol // should still see her balance as the push amount, which is 1/4 of the // capacity. carolChan := ht.AssertChannelLocalBalance( carol, chanPoint, int64(chanAmt/4), ) // Grab Carol's current commitment height (update number), we'll later // revert her to this state after additional updates to force him to // broadcast this soon to be revoked state. carolStateNumPreCopy := int(carolChan.NumUpdates) // With the temporary file created, copy Carol's current state into the // temporary file we created above. Later after more updates, we'll // restore this state. ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. ht.EnsureConnected(dave, carol) // Once connected, give Dave some time to enable the channel again. ht.AssertTopologyChannelOpen(dave, chanPoint) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint) daveBalResp := dave.RPC.WalletBalance() davePreSweepBalance := daveBalResp.ConfirmedBalance // Wait until the backup has been accepted by the watchtower before // shutting down Dave. err := wait.NoError(func() error { bkpStats := dave.RPC.WatchtowerStats() if bkpStats == nil { return errors.New("no active backup sessions") } if bkpStats.NumBackups == 0 { return errors.New("no backups accepted") } return nil }, defaultTimeout) require.NoError(ht, err, "unable to verify backup task completed") // Shutdown Dave to simulate going offline for an extended period of // time. Once he's not watching, Carol will try to breach the channel. restart := ht.SuspendNode(dave) // Now we shutdown Carol, copying over the his temporary database state // which has the *prior* channel state over his current most up to date // state. With this, we essentially force Carol to travel back in time // within the channel's history. ht.RestartNodeAndRestoreDB(carol) // Now query for Carol's channel state, it should show that he's at a // state number in the past, not the *latest* state. ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy) // Now force Carol to execute a *force* channel closure by unilaterally // broadcasting his current channel state. This is actually the // commitment transaction of a prior *revoked* state, so he'll soon // feel the wrath of Dave's retribution. closeUpdates, closeTxID := ht.CloseChannelAssertPending( carol, chanPoint, true, ) // Finally, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] breachTXID := ht.WaitForChannelCloseEvent(closeUpdates) ht.AssertTxInBlock(block, breachTXID) // The breachTXID should match the above closeTxID. require.EqualValues(ht, breachTXID, closeTxID) // Query the mempool for Dave's justice transaction, this should be // broadcast as Carol's contract breaching transaction gets confirmed // above. justiceTXID := ht.AssertNumTxsInMempool(1)[0] // Query for the mempool transaction found above. Then assert that all // the inputs of this transaction are spending outputs generated by // Carol's breach transaction above. justiceTx := ht.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], "justice tx not spending commitment utxo") } willyBalResp := willy.WalletBalance() require.Zero(ht, willyBalResp.ConfirmedBalance, "willy should have 0 balance before mining justice transaction") // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() require.Equal(ht, justiceTx.Hash()[:], justiceSha[:], "justice tx wasn't mined") // Ensure that Willy doesn't get any funds, as he is acting as an // altruist watchtower. err = wait.NoError(func() error { willyBalResp := willy.WalletBalance() if willyBalResp.ConfirmedBalance != 0 { return fmt.Errorf("expected Willy to have no funds "+ "after justice transaction was mined, found %v", willyBalResp) } return nil }, time.Second*5) require.NoError(ht, err, "timeout checking willy's balance") // Before restarting Dave, shutdown Carol so Dave won't sync with her. // Otherwise, during the restart, Dave will realize Carol is falling // behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing // the channel. Although this force close tx will be later replaced by // the breach tx, it will create two anchor sweeping txes for neutrino // backend, causing the confirmed wallet balance to be zero later on // because the utxos are used in sweeping. ht.Shutdown(carol) // Restart Dave, who will still think his channel with Carol is open. // We should him to detect the breach, but realize that the funds have // then been swept to his wallet by Willy. require.NoError(ht, restart(), "unable to restart dave") err = wait.NoError(func() error { daveBalResp := dave.RPC.ChannelBalance() if daveBalResp.LocalBalance.Sat != 0 { return fmt.Errorf("Dave should end up with zero "+ "channel balance, instead has %d", daveBalResp.LocalBalance.Sat) } return nil }, defaultTimeout) require.NoError(ht, err, "timeout checking dave's channel balance") ht.AssertNumPendingForceClose(dave, 0) // If this is an anchor channel, Dave would offer his sweeper the // anchor. However, due to no time-sensitive outputs involved, the // anchor sweeping won't happen as it's uneconomical. if lntest.CommitTypeHasAnchors(commitType) { ht.AssertNumPendingSweeps(dave, 1) // Mine a block to trigger the sweep. ht.MineEmptyBlocks(1) } // Check that Dave's wallet balance is increased. err = wait.NoError(func() error { daveBalResp := dave.RPC.WalletBalance() if daveBalResp.ConfirmedBalance <= davePreSweepBalance { return fmt.Errorf("Dave should have more than %d "+ "after sweep, instead has %d", davePreSweepBalance, daveBalResp.ConfirmedBalance) } return nil }, defaultTimeout) require.NoError(ht, err, "timeout checking dave's wallet balance") // Dave should have no open channels. ht.AssertNodeNumChannels(dave, 0) } func setUpNewTower(ht *lntest.HarnessTest, name, externalIP string) ([]byte, string, *rpc.HarnessRPC) { port := port.NextAvailablePort() listenAddr := fmt.Sprintf("0.0.0.0:%d", port) // Set up the new watchtower. tower := ht.NewNode(name, []string{ "--watchtower.active", "--watchtower.externalip=" + externalIP, "--watchtower.listen=" + listenAddr, }) towerInfo := tower.RPC.GetInfoWatchtower() require.Len(ht, towerInfo.Listeners, 1) listener := towerInfo.Listeners[0] require.True( ht, listener == listenAddr || listener == fmt.Sprintf("[::]:%d", port), ) // Assert the Tower's URIs properly display the chosen external IP. require.Len(ht, towerInfo.Uris, 1) require.Contains(ht, towerInfo.Uris[0], externalIP) return towerInfo.Pubkey, towerInfo.Listeners[0], tower.RPC } // generateBackups is a helper function that can be used to create a number of // watchtower back-ups. func generateBackups(ht *lntest.HarnessTest, srcNode, dstNode *node.HarnessNode, numBackups int64) { const paymentAmt = 10_000 require.EqualValuesf(ht, numBackups%2, 0, "the number of desired "+ "back-ups must be even") // Two updates are made for every payment. numPayments := int(numBackups / 2) // Create the required number of invoices. alicePayReqs, _, _ := ht.CreatePayReqs( dstNode, paymentAmt, numPayments, ) 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, ) } // Pay each invoice. for i := 0; i < numPayments; i++ { send(srcNode, alicePayReqs[i]) } } // assertNumBackups is a helper that asserts that the given node 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. func assertNumBackups(ht *lntest.HarnessTest, node *rpc.HarnessRPC, towerPk []byte, expectedNumBackups int, mineOnFail bool) { err := wait.NoError(func() error { info := node.GetTowerInfo( &wtclientrpc.GetTowerInfoRequest{ Pubkey: towerPk, IncludeSessions: true, }, ) var numBackups uint32 for _, sessionType := range info.SessionInfo { for _, session := range sessionType.Sessions { numBackups += session.NumBackups } } if numBackups == uint32(expectedNumBackups) { return nil } if mineOnFail { ht.MineBlocks(1) } return fmt.Errorf("expected %d backups, got %d", expectedNumBackups, numBackups) }, defaultTimeout) require.NoError(ht, err) }