diff --git a/lntemp/rpc/watchtower.go b/lntemp/rpc/watchtower.go index c18734d94..1b05d15ea 100644 --- a/lntemp/rpc/watchtower.go +++ b/lntemp/rpc/watchtower.go @@ -1,5 +1,52 @@ package rpc +import ( + "context" + + "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" +) + // ===================== // WatchtowerClient and WatchtowerClientClient related RPCs. // ===================== + +// GetInfoWatchtower makes a RPC call to the watchtower of the given node and +// asserts. +func (h *HarnessRPC) GetInfoWatchtower() *watchtowerrpc.GetInfoResponse { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &watchtowerrpc.GetInfoRequest{} + info, err := h.Watchtower.GetInfo(ctxt, req) + h.NoError(err, "GetInfo from Watchtower") + + return info +} + +// AddTower makes a RPC call to the WatchtowerClient of the given node and +// asserts. +func (h *HarnessRPC) AddTower( + req *wtclientrpc.AddTowerRequest) *wtclientrpc.AddTowerResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WatchtowerClient.AddTower(ctxt, req) + h.NoError(err, "AddTower") + + return resp +} + +// WatchtowerStats makes a RPC call to the WatchtowerClient of the given node +// and asserts. +func (h *HarnessRPC) WatchtowerStats() *wtclientrpc.StatsResponse { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &wtclientrpc.StatsRequest{} + resp, err := h.WatchtowerClient.Stats(ctxt, req) + h.NoError(err, "Stats from Watchtower") + + return resp +} diff --git a/lntest/itest/list_on_test.go b/lntest/itest/list_on_test.go index 6b4494f96..2b3acbf06 100644 --- a/lntest/itest/list_on_test.go +++ b/lntest/itest/list_on_test.go @@ -304,4 +304,8 @@ var allTestCasesTemp = []*lntemp.TestCase{ Name: "revoked uncooperative close retribution remote hodl", TestFunc: testRevokedCloseRetributionRemoteHodl, }, + { + Name: "revoked uncooperative close retribution altruist watchtower", + TestFunc: testRevokedCloseRetributionAltruistWatchtower, + }, } diff --git a/lntest/itest/lnd_revocation_test.go b/lntest/itest/lnd_revocation_test.go index 24473bc17..20561c039 100644 --- a/lntest/itest/lnd_revocation_test.go +++ b/lntest/itest/lnd_revocation_test.go @@ -2,9 +2,7 @@ package itest import ( "bytes" - "context" "fmt" - "strings" "testing" "time" @@ -13,7 +11,6 @@ import ( "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lntest" @@ -581,9 +578,7 @@ func testRevokedCloseRetributionRemoteHodl(ht *lntemp.HarnessTest) { // 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(net *lntest.NetworkHarness, - t *harnessTest) { - +func testRevokedCloseRetributionAltruistWatchtower(ht *lntemp.HarnessTest) { testCases := []struct { name string anchors bool @@ -597,23 +592,25 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, for _, tc := range testCases { tc := tc + testFunc := func(ht *lntemp.HarnessTest) { + testRevokedCloseRetributionAltruistWatchtowerCase( + ht, tc.anchors, + ) + } - success := t.t.Run(tc.name, func(tt *testing.T) { - ht := newHarnessTest(tt, net) - ht.RunTestCase(&testCase{ - name: tc.name, - test: func(net1 *lntest.NetworkHarness, t1 *harnessTest) { - testRevokedCloseRetributionAltruistWatchtowerCase( - net1, t1, tc.anchors, - ) - }, + success := ht.Run(tc.name, func(tt *testing.T) { + st := ht.Subtest(tt) + + st.RunTestCase(&lntemp.TestCase{ + Name: tc.name, + TestFunc: testFunc, }) }) if !success { // Log failure time to help relate the lnd logs to the // failure. - t.Logf("Failure time: %v", time.Now().Format( + ht.Logf("Failure time: %v", time.Now().Format( "2006-01-02 15:04:05.000", )) @@ -622,10 +619,9 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, } } -func testRevokedCloseRetributionAltruistWatchtowerCase( - net *lntest.NetworkHarness, t *harnessTest, anchors bool) { +func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntemp.HarnessTest, + anchors bool) { - ctxb := context.Background() const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 @@ -639,49 +635,32 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( if anchors { carolArgs = append(carolArgs, "--protocol.anchors") } - carol := net.NewNode(t.t, "Carol", carolArgs) - defer shutdownAndAssert(net, t, carol) + 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 := net.NewNode(t.t, "Willy", []string{ - "--watchtower.active", - "--watchtower.externalip=" + externalIP, - }) - defer shutdownAndAssert(net, t, willy) - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - willyInfo, err := willy.Watchtower.GetInfo( - ctxt, &watchtowerrpc.GetInfoRequest{}, + willy := ht.NewNode( + "Willy", []string{"--watchtower.active", + "--watchtower.externalip=" + externalIP}, ) - if err != nil { - t.Fatalf("unable to getinfo from willy: %v", err) - } + + 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. - if len(willyInfo.Listeners) != 1 { - t.Fatalf("Willy should have 1 listener, has %d", - len(willyInfo.Listeners)) - } + require.Len(ht, willyInfo.Listeners, 1, "Willy should have 1 listener") listener := willyInfo.Listeners[0] if listener != "0.0.0.0:9911" && listener != "[::]:9911" { - t.Fatalf("expected listener on 0.0.0.0:9911 or [::]: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. - if len(willyInfo.Uris) != 1 { - t.Fatalf("Willy should have 1 uri, has %d", - len(willyInfo.Uris)) - } - if !strings.Contains(willyInfo.Uris[0], externalIP) { - t.Fatalf("expected uri with %v, got %v", - externalIP, willyInfo.Uris[0]) - } + 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 @@ -693,110 +672,67 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( if anchors { daveArgs = append(daveArgs, "--protocol.anchors") } - dave := net.NewNode(t.t, "Dave", daveArgs) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("Dave", daveArgs) - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) addTowerReq := &wtclientrpc.AddTowerRequest{ Pubkey: willyInfo.Pubkey, Address: listener, } - if _, err := dave.WatchtowerClient.AddTower(ctxt, addTowerReq); err != nil { - t.Fatalf("unable to add willy's watchtower: %v", err) - } + 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, - net.ConnectNodes(t.t, dave, carol) + ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) + 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. - chanPoint := openChannelAndAssert( - t, net, dave, carol, - lntest.OpenChannelParams{ - Amt: 3 * (chanAmt / 4), - PushAmt: chanAmt / 4, - }, - ) + params := lntemp.OpenChannelParams{ + Amt: 3 * (chanAmt / 4), + PushAmt: chanAmt / 4, + } + 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, _, _, err := createPayReqs( - carol, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // Wait for Dave to receive the channel edge from the funding manager. - err = dave.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("dave didn't see the dave->carol channel before "+ - "timeout: %v", err) - } + 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, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } - if carolChan.LocalBalance != int64(chanAmt/4) { - t.Fatalf("carol's balance is incorrect, got %v, expected %v", - carolChan.LocalBalance, chanAmt/4) - } + 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 := carolChan.NumUpdates + 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. - if err := net.BackupDb(carol); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. - net.EnsureConnected(t.t, dave, carol) + ht.EnsureConnected(dave, carol) - // Finally, send payments from Dave to Carol, consuming Carol's remaining - // payment hashes. - err = completePaymentRequests( - dave, dave.RouterClient, carolPayReqs, false, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } - - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + // 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 { - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - bkpStats, err := dave.WatchtowerClient.Stats(ctxt, - &wtclientrpc.StatsRequest{}, - ) - if err != nil { - return err - } + err := wait.NoError(func() error { + bkpStats := dave.RPC.WatchtowerStats() if bkpStats == nil { return errors.New("no active backup sessions") } @@ -806,197 +742,128 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( return nil }, defaultTimeout) - if err != nil { - t.Fatalf("unable to verify backup task completed: %v", err) - } + 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, err := net.SuspendNode(dave) - if err != nil { - t.Fatalf("unable to suspend Dave: %v", err) - } + 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. - if err = net.RestartNode(carol, func() error { - return net.RestoreDb(carol) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } + 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. - carolChan, err = getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol chan info: %v", err) - } - if carolChan.NumUpdates != carolStateNumPreCopy { - t.Fatalf("db copy failed: %v", carolChan.NumUpdates) - } + 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, err := net.CloseChannel(carol, chanPoint, true) - if err != nil { - t.Fatalf("unable to close channel: %v", err) - } - - // Query the mempool for the breaching closing transaction, this should - // be broadcast by Carol when she force closes the channel above. - txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's force close tx in mempool: %v", - err) - } - if *txid != *closeTxID { - t.Fatalf("expected closeTx(%v) in mempool, instead found %v", - closeTxID, txid) - } + 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 := mineBlocks(t, net, 1, 1)[0] + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - breachTXID, err := net.WaitForChannelClose(closeUpdates) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) - } - assertTxInBlock(t, block, breachTXID) + 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, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Dave's justice tx in mempool: %v", - err) - } - time.Sleep(100 * time.Millisecond) + 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, err := net.Miner.Client.GetRawTransaction(justiceTXID) - if err != nil { - t.Fatalf("unable to query for justice tx: %v", err) - } + justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { - if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { - t.Fatalf("justice tx not spending commitment utxo "+ - "instead is: %v", txIn.PreviousOutPoint) - } + require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], + "justice tx not spending commitment utxo") } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - willyBalReq := &lnrpc.WalletBalanceRequest{} - willyBalResp, err := willy.WalletBalance(ctxt, willyBalReq) - if err != nil { - t.Fatalf("unable to get willy's balance: %v", err) - } - - if willyBalResp.ConfirmedBalance != 0 { - t.Fatalf("willy should have 0 balance before mining "+ - "justice transaction, instead has %d", - willyBalResp.ConfirmedBalance) - } + 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 = mineBlocks(t, net, 1, 1)[0] + block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. - if len(block.Transactions) != 2 { - t.Fatalf("transaction wasn't mined") - } + require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() - if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { - t.Fatalf("justice tx wasn't mined") - } + 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. - var predErr error - err = wait.Invariant(func() bool { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - willyBalReq := &lnrpc.WalletBalanceRequest{} - willyBalResp, err := willy.WalletBalance(ctxt, willyBalReq) - if err != nil { - t.Fatalf("unable to get willy's balance: %v", err) - } + err = wait.NoError(func() error { + willyBalResp := willy.RPC.WalletBalance() if willyBalResp.ConfirmedBalance != 0 { - predErr = fmt.Errorf("Expected Willy to have no funds "+ + return fmt.Errorf("Expected Willy to have no funds "+ "after justice transaction was mined, found %v", willyBalResp) - return false } - return true + return nil }, time.Second*5) - if err != nil { - t.Fatalf("%v", predErr) - } + 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. - err = restart() - if err != nil { - t.Fatalf("unable to restart dave: %v", err) - } - - err = wait.Predicate(func() bool { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.ChannelBalanceRequest{} - daveBalResp, err := dave.ChannelBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + require.NoError(ht, restart(), "unable to restart dave") + err = wait.NoError(func() error { + daveBalResp := dave.RPC.ChannelBalance() if daveBalResp.LocalBalance.Sat != 0 { - predErr = fmt.Errorf("Dave should end up with zero "+ + return fmt.Errorf("Dave should end up with zero "+ "channel balance, instead has %d", daveBalResp.LocalBalance.Sat) - return false } - return true + return nil }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + require.NoError(ht, err, "timeout checking dave's channel balance") - assertNumPendingChannels(t, dave, 0, 0) + ht.AssertNumPendingForceClose(dave, 0) - err = wait.Predicate(func() bool { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + // Check that Dave's wallet balance is increased. + err = wait.NoError(func() error { + daveBalResp := dave.RPC.WalletBalance() if daveBalResp.ConfirmedBalance <= davePreSweepBalance { - predErr = fmt.Errorf("Dave should have more than %d "+ + return fmt.Errorf("Dave should have more than %d "+ "after sweep, instead has %d", davePreSweepBalance, daveBalResp.ConfirmedBalance) - return false } - return true + return nil }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + require.NoError(ht, err, "timeout checking dave's wallet balance") // Dave should have no open channels. - assertNodeNumChannels(t, dave, 0) + ht.AssertNodeNumChannels(dave, 0) } diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 926c3fca2..84a629795 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -72,10 +72,6 @@ var allTestCases = []*testCase{ name: "switch offline delivery outgoing offline", test: testSwitchOfflineDeliveryOutgoingOffline, }, - { - name: "revoked uncooperative close retribution altruist watchtower", - test: testRevokedCloseRetributionAltruistWatchtower, - }, { name: "query routes", test: testQueryRoutes,