From d4424fbcfaf8107f35199fefd9879d44593824af Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 9 Feb 2024 12:45:36 +0200 Subject: [PATCH] itest: refactor watchtower related tests --- itest/list_on_test.go | 12 +- itest/lnd_revocation_test.go | 303 ----------------------------- itest/lnd_watchtower_test.go | 357 ++++++++++++++++++++++++++++++++--- 3 files changed, 335 insertions(+), 337 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index e8cf80c5f..c7e76a52f 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -519,16 +519,8 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testLookupHtlcResolution, }, { - Name: "watchtower session management", - TestFunc: testWatchtowerSessionManagement, - }, - { - // NOTE: this test must be put in the same tranche as - // `testWatchtowerSessionManagement` to avoid parallel use of - // the default watchtower port. - Name: "revoked uncooperative close retribution altruist " + - "watchtower", - TestFunc: testRevokedCloseRetributionAltruistWatchtower, + Name: "watchtower", + TestFunc: testWatchtower, }, { Name: "channel fundmax", diff --git a/itest/lnd_revocation_test.go b/itest/lnd_revocation_test.go index d02e60d7e..d94fa7c43 100644 --- a/itest/lnd_revocation_test.go +++ b/itest/lnd_revocation_test.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "testing" - "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -12,7 +11,6 @@ import ( "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" @@ -707,304 +705,3 @@ func testRevokedCloseRetributionRemoteHodl(ht *lntest.HarnessTest) { }) } } - -// 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) - - // Willy the watchtower 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. - willy := ht.NewNode( - "Willy", []string{ - "--watchtower.active", - "--watchtower.externalip=" + externalIP, - }, - ) - - willyInfo := willy.RPC.GetInfoWatchtower() - - // Assert that Willy 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, willyInfo.Listeners, 1, "Willy should have 1 listener") - listener := willyInfo.Listeners[0] - if listener != "0.0.0.0:9911" && listener != "[::]:9911" { - ht.Fatalf("expected listener on 0.0.0.0:9911 or [::]:9911, "+ - "got %v", listener) - } - - // Assert the Willy's URIs properly display the chosen external IP. - require.Len(ht, willyInfo.Uris, 1, "Willy should have 1 uri") - require.Contains(ht, willyInfo.Uris[0], 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: willyInfo.Pubkey, - 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.Miner.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.Miner.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.Miner.GetRawTransaction(justiceTXID) - for _, txIn := range justiceTx.MsgTx().TxIn { - require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], - "justice tx not spending commitment utxo") - } - - willyBalResp := willy.RPC.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.RPC.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 sweep the anchor. - if lntest.CommitTypeHasAnchors(commitType) { - ht.MineBlocksAndAssertNumTxes(1, 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) -} diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go index 3432848d8..efd14503b 100644 --- a/itest/lnd_watchtower_test.go +++ b/itest/lnd_watchtower_test.go @@ -2,21 +2,38 @@ 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/rpc" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) -// testWatchtowerSessionManagement tests that session deletion is done -// correctly. -func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { +// 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) + }) +} + +// testTowerClientSessionDeletion tests that sessions are correctly deleted +// when they are deemed closable. +func testTowerClientSessionDeletion(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10_000 @@ -28,24 +45,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { // 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) + wallisPk, listener, _ := setUpNewTower(ht, "Wallis", externalIP) // Dave will be the tower client. daveArgs := []string{ @@ -58,7 +58,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { dave := ht.NewNode("Dave", daveArgs) addTowerReq := &wtclientrpc.AddTowerRequest{ - Pubkey: wallisInfo.Pubkey, + Pubkey: wallisPk, Address: listener, } dave.RPC.AddTower(addTowerReq) @@ -66,7 +66,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { // Assert that there exists a session between Dave and Wallis. err := wait.NoError(func() error { info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{ - Pubkey: wallisInfo.Pubkey, + Pubkey: wallisPk, IncludeSessions: true, }) @@ -125,7 +125,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { err = wait.NoError(func() error { info := dave.RPC.GetTowerInfo( &wtclientrpc.GetTowerInfoRequest{ - Pubkey: wallisInfo.Pubkey, + Pubkey: wallisPk, IncludeSessions: true, }, ) @@ -170,3 +170,312 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) { // ensure that the session deleting logic is run. assertNumBackups(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.Miner.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.Miner.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.Miner.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 sweep the anchor. + if lntest.CommitTypeHasAnchors(commitType) { + ht.MineBlocksAndAssertNumTxes(1, 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 := node.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 +}