diff --git a/lntest/itest/assertions.go b/lntest/itest/assertions.go index 41a147599..64b304760 100644 --- a/lntest/itest/assertions.go +++ b/lntest/itest/assertions.go @@ -1101,198 +1101,6 @@ func assertNumPendingChannels(t *harnessTest, node *lntest.HarnessNode, require.NoErrorf(t.t, err, "got err: %v", predErr) } -// assertDLPExecuted asserts that Dave is a node that has recovered their state -// form scratch. Carol should then force close on chain, with Dave sweeping his -// funds immediately, and Carol sweeping her fund after her CSV delay is up. If -// the blankSlate value is true, then this means that Dave won't need to sweep -// on chain as he has no funds in the channel. -func assertDLPExecutedOld(net *lntest.NetworkHarness, t *harnessTest, - carol *lntest.HarnessNode, carolStartingBalance int64, - dave *lntest.HarnessNode, daveStartingBalance int64, - commitType lnrpc.CommitmentType) { - - // Increase the fee estimate so that the following force close tx will - // be cpfp'ed. - net.SetFeeEstimate(30000) - - // We disabled auto-reconnect for some tests to avoid timing issues. - // To make sure the nodes are initiating DLP now, we have to manually - // re-connect them. - ctxb := context.Background() - net.EnsureConnected(t.t, carol, dave) - - // Upon reconnection, the nodes should detect that Dave is out of sync. - // Carol should force close the channel using her latest commitment. - expectedTxes := 1 - if commitTypeHasAnchors(commitType) { - expectedTxes = 2 - } - _, err := waitForNTxsInMempool( - net.Miner.Client, expectedTxes, minerMempoolTimeout, - ) - require.NoError( - t.t, err, - "unable to find Carol's force close tx in mempool", - ) - - // Channel should be in the state "waiting close" for Carol since she - // broadcasted the force close tx. - assertNumPendingChannels(t, carol, 1, 0) - - // Dave should also consider the channel "waiting close", as he noticed - // the channel was out of sync, and is now waiting for a force close to - // hit the chain. - assertNumPendingChannels(t, dave, 1, 0) - - // Restart Dave to make sure he is able to sweep the funds after - // shutdown. - require.NoError(t.t, net.RestartNode(dave, nil), "Node restart failed") - - // Generate a single block, which should confirm the closing tx. - _ = mineBlocks(t, net, 1, expectedTxes)[0] - - // Dave should consider the channel pending force close (since he is - // waiting for his sweep to confirm). - assertNumPendingChannels(t, dave, 0, 1) - - // Carol is considering it "pending force close", as we must wait - // before she can sweep her outputs. - assertNumPendingChannels(t, carol, 0, 1) - - if commitType == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { - // Dave should sweep his anchor only, since he still has the - // lease CLTV constraint on his commitment output. - _, err = waitForNTxsInMempool( - net.Miner.Client, 1, minerMempoolTimeout, - ) - require.NoError(t.t, err, "unable to find Dave's anchor sweep "+ - "tx in mempool") - - // Mine Dave's anchor sweep tx. - _ = mineBlocks(t, net, 1, 1)[0] - - // After Carol's output matures, she should also reclaim her - // funds. - // - // The commit sweep resolver publishes the sweep tx at - // defaultCSV-1 and we already mined one block after the - // commitmment was published, so take that into account. - mineBlocks(t, net, defaultCSV-1-1, 0) - carolSweep, err := waitForTxInMempool( - net.Miner.Client, minerMempoolTimeout, - ) - require.NoError(t.t, err, "unable to find Carol's sweep tx in "+ - "mempool") - block := mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, carolSweep) - - // Now the channel should be fully closed also from Carol's POV. - assertNumPendingChannels(t, carol, 0, 0) - - // We'll now mine the remaining blocks to prompt Dave to sweep - // his CLTV-constrained output. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - resp, err := dave.PendingChannels( - ctxt, &lnrpc.PendingChannelsRequest{}, - ) - require.NoError(t.t, err) - blocksTilMaturity := - resp.PendingForceClosingChannels[0].BlocksTilMaturity - require.Positive(t.t, blocksTilMaturity) - - mineBlocks(t, net, uint32(blocksTilMaturity), 0) - daveSweep, err := waitForTxInMempool( - net.Miner.Client, minerMempoolTimeout, - ) - require.NoError(t.t, err, "unable to find Dave's sweep tx in "+ - "mempool") - block = mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, daveSweep) - - // Now Dave should consider the channel fully closed. - assertNumPendingChannels(t, dave, 0, 0) - } else { - // Dave should sweep his funds immediately, as they are not - // timelocked. We also expect Dave to sweep his anchor, if - // present. - _, err = waitForNTxsInMempool( - net.Miner.Client, expectedTxes, minerMempoolTimeout, - ) - require.NoError(t.t, err, "unable to find Dave's sweep tx in "+ - "mempool") - - // Mine the sweep tx. - _ = mineBlocks(t, net, 1, expectedTxes)[0] - - // Now Dave should consider the channel fully closed. - assertNumPendingChannels(t, dave, 0, 0) - - // After Carol's output matures, she should also reclaim her - // funds. - // - // The commit sweep resolver publishes the sweep tx at - // defaultCSV-1 and we already mined one block after the - // commitmment was published, so take that into account. - mineBlocks(t, net, defaultCSV-1-1, 0) - carolSweep, err := waitForTxInMempool( - net.Miner.Client, minerMempoolTimeout, - ) - require.NoError(t.t, err, "unable to find Carol's sweep tx in "+ - "mempool") - block := mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, carolSweep) - - // Now the channel should be fully closed also from Carol's POV. - assertNumPendingChannels(t, carol, 0, 0) - } - - // We query Dave's balance to make sure it increased after the channel - // closed. This checks that he was able to sweep the funds he had in - // the channel. - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - balReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, balReq) - require.NoError(t.t, err, "unable to get dave's balance") - - daveBalance := daveBalResp.ConfirmedBalance - require.Greater( - t.t, daveBalance, daveStartingBalance, "balance not increased", - ) - - // Make sure Carol got her balance back. - err = wait.NoError(func() error { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolBalResp, err := carol.WalletBalance(ctxt, balReq) - if err != nil { - return fmt.Errorf("unable to get carol's balance: %v", err) - } - - carolBalance := carolBalResp.ConfirmedBalance - - // With Neutrino we don't get a backend error when trying to - // publish an orphan TX (which is what the sweep for the remote - // anchor is since the remote commitment TX was not broadcast). - // That's why the wallet still sees that as unconfirmed and we - // need to count the total balance instead of the confirmed. - if net.BackendCfg.Name() == lntest.NeutrinoBackendName { - carolBalance = carolBalResp.TotalBalance - } - - if carolBalance <= carolStartingBalance { - return fmt.Errorf("expected carol to have balance "+ - "above %d, instead had %v", carolStartingBalance, - carolBalance) - } - - return nil - }, defaultTimeout) - require.NoError(t.t, err) - - assertNodeNumChannels(t, dave, 0) - assertNodeNumChannels(t, carol, 0) -} - // verifyCloseUpdate is used to verify that a closed channel update is of the // expected type. func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate, diff --git a/lntest/itest/list_on_test.go b/lntest/itest/list_on_test.go index bd4d2d392..939ffc2fa 100644 --- a/lntest/itest/list_on_test.go +++ b/lntest/itest/list_on_test.go @@ -23,4 +23,8 @@ var allTestCasesTemp = []*lntemp.TestCase{ Name: "channel backup restore", TestFunc: testChannelBackupRestore, }, + { + Name: "data loss protection", + TestFunc: testDataLossProtection, + }, } diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index 8e8456588..7c244b790 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -1167,8 +1167,7 @@ func testChanRestoreScenario(ht *lntemp.HarnessTest, // relationship lost state, they will detect this during channel sync, and the // up-to-date party will force close the channel, giving the outdated party the // opportunity to sweep its output. -func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() +func testDataLossProtection(ht *lntemp.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 @@ -1180,36 +1179,31 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { // protection logic automatically. We also can't have Carol // automatically re-connect too early, otherwise DLP would be initiated // at the wrong moment. - carol := net.NewNode( - t.t, "Carol", []string{"--nolisten", "--minbackoff=1h"}, - ) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", []string{"--nolisten", "--minbackoff=1h"}) // Dave will be the party losing his state. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("Dave", nil) // Before we make a channel, we'll load up Carol with some coins sent // directly from the miner. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // timeTravel is a method that will make Carol open a channel to the // passed node, settle a series of payments, then reset the node back // to the state before the payments happened. When this method returns // the node will be unaware of the new state updates. The returned // function can be used to restart the node in this state. - timeTravel := func(node *lntest.HarnessNode) (func() error, - *lnrpc.ChannelPoint, int64, error) { + timeTravel := func(node *node.HarnessNode) (func() error, + *lnrpc.ChannelPoint, int64) { // We must let the node communicate with Carol before they are // able to open channel, so we connect them. - net.EnsureConnected(t.t, carol, node) + ht.EnsureConnected(carol, node) // We'll first open up a channel between them with a 0.5 BTC // value. - chanPoint := openChannelAndAssert( - t, net, carol, node, - lntest.OpenChannelParams{ + chanPoint := ht.OpenChannel( + carol, node, lntemp.OpenChannelParams{ Amt: chanAmt, }, ) @@ -1219,54 +1213,18 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { // the channel. // TODO(halseth): have dangling HTLCs on the commitment, able to // retrieve funds? - payReqs, _, _, err := createPayReqs( - node, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // Wait for Carol to receive the channel edge from the funding - // manager. - err = carol.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("carol didn't see the carol->%s channel "+ - "before timeout: %v", node.Name(), err) - } + payReqs, _, _ := ht.CreatePayReqs(node, paymentAmt, numInvoices) // Send payments from Carol using 3 of the payment hashes // generated above. - err = completePaymentRequests( - carol, carol.RouterClient, - payReqs[:numInvoices/2], true, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } + ht.CompletePaymentRequests(carol, payReqs[:numInvoices/2]) // Next query for the node's channel state, as we sent 3 // payments of 10k satoshis each, it should now see his balance // as being 30k satoshis. - var nodeChan *lnrpc.Channel - var predErr error - err = wait.Predicate(func() bool { - bChan, err := getChanInfo(node) - if err != nil { - t.Fatalf("unable to get channel info: %v", err) - } - if bChan.LocalBalance != 30000 { - predErr = fmt.Errorf("balance is incorrect, "+ - "got %v, expected %v", - bChan.LocalBalance, 30000) - return false - } - - nodeChan = bChan - return true - }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + nodeChan := ht.AssertChannelLocalBalance( + node, chanPoint, 30_000, + ) // Grab the current commitment height (update number), we'll // later revert him to this state after additional updates to @@ -1276,94 +1234,53 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { // With the temporary file created, copy the current state into // the temporary file we created above. Later after more // updates, we'll restore this state. - if err := net.BackupDb(node); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(node) - // Reconnect the peers after the restart that was needed for the db - // backup. - net.EnsureConnected(t.t, carol, node) + // Reconnect the peers after the restart that was needed for + // the db backup. + ht.EnsureConnected(carol, node) // Finally, send more payments from , using the remaining // payment hashes. - err = completePaymentRequests( - carol, carol.RouterClient, payReqs[numInvoices/2:], true, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } - - nodeChan, err = getChanInfo(node) - if err != nil { - t.Fatalf("unable to get dave chan info: %v", err) - } + ht.CompletePaymentRequests(carol, payReqs[numInvoices/2:]) // Now we shutdown the node, copying over the its temporary // database state which has the *prior* channel state over his // current most up to date state. With this, we essentially // force the node to travel back in time within the channel's // history. - if err = net.RestartNode(node, func() error { - return net.RestoreDb(node) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } + ht.RestartNodeAndRestoreDB(node) // Make sure the channel is still there from the PoV of the // node. - assertNodeNumChannels(t, node, 1) + ht.AssertNodeNumChannels(node, 1) // Now query for the channel state, it should show that it's at // a state number in the past, not the *latest* state. - nodeChan, err = getChanInfo(node) - if err != nil { - t.Fatalf("unable to get dave chan info: %v", err) - } - if nodeChan.NumUpdates != stateNumPreCopy { - t.Fatalf("db copy failed: %v", nodeChan.NumUpdates) - } + ht.AssertChannelNumUpdates(node, stateNumPreCopy, chanPoint) - balReq := &lnrpc.WalletBalanceRequest{} - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - balResp, err := node.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + balResp := node.RPC.WalletBalance() + restart := ht.SuspendNode(node) - restart, err := net.SuspendNode(node) - if err != nil { - t.Fatalf("unable to suspend node: %v", err) - } - - return restart, chanPoint, balResp.ConfirmedBalance, nil + return restart, chanPoint, balResp.ConfirmedBalance } // Reset Dave to a state where he has an outdated channel state. - restartDave, _, daveStartingBalance, err := timeTravel(dave) - if err != nil { - t.Fatalf("unable to time travel dave: %v", err) - } + restartDave, _, daveStartingBalance := timeTravel(dave) // We make a note of the nodes' current on-chain balances, to make sure // they are able to retrieve the channel funds eventually, - balReq := &lnrpc.WalletBalanceRequest{} - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - carolBalResp, err := carol.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get carol's balance: %v", err) - } + carolBalResp := carol.RPC.WalletBalance() carolStartingBalance := carolBalResp.ConfirmedBalance // Restart Dave to trigger a channel resync. - if err := restartDave(); err != nil { - t.Fatalf("unable to restart dave: %v", err) - } + require.NoError(ht, restartDave(), "unable to restart dave") // Assert that once Dave comes up, they reconnect, Carol force closes // on chain, and both of them properly carry out the DLP protocol. - assertDLPExecutedOld( - net, t, carol, carolStartingBalance, dave, daveStartingBalance, - lnrpc.CommitmentType_STATIC_REMOTE_KEY, + assertDLPExecuted( + ht, carol, carolStartingBalance, dave, + daveStartingBalance, lnrpc.CommitmentType_STATIC_REMOTE_KEY, ) // As a second part of this test, we will test the scenario where a @@ -1373,93 +1290,47 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { // closed channel, such that Dave can retrieve his funds. // // We start by letting Dave time travel back to an outdated state. - restartDave, chanPoint2, daveStartingBalance, err := timeTravel(dave) - if err != nil { - t.Fatalf("unable to time travel eve: %v", err) - } + restartDave, chanPoint2, daveStartingBalance := timeTravel(dave) - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolBalResp, err = carol.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get carol's balance: %v", err) - } + carolBalResp = carol.RPC.WalletBalance() carolStartingBalance = carolBalResp.ConfirmedBalance // Now let Carol force close the channel while Dave is offline. - closeChannelAndAssert(t, net, carol, chanPoint2, true) - - // Wait for the channel to be marked pending force close. - err = waitForChannelPendingForceClose(carol, chanPoint2) - if err != nil { - t.Fatalf("channel not pending force close: %v", err) - } - - // Mine enough blocks for Carol to sweep her funds. - mineBlocks(t, net, defaultCSV-1, 0) - - carolSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err) - } - block := mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, carolSweep) - - // Now the channel should be fully closed also from Carol's POV. - assertNumPendingChannels(t, carol, 0, 0) + ht.ForceCloseChannel(carol, chanPoint2) // Make sure Carol got her balance back. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolBalResp, err = carol.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get carol's balance: %v", err) - } + carolBalResp = carol.RPC.WalletBalance() carolBalance := carolBalResp.ConfirmedBalance - if carolBalance <= carolStartingBalance { - t.Fatalf("expected carol to have balance above %d, "+ - "instead had %v", carolStartingBalance, - carolBalance) - } + require.Greater(ht, carolBalance, carolStartingBalance, + "expected carol to have balance increased") - assertNodeNumChannels(t, carol, 0) + ht.AssertNodeNumChannels(carol, 0) // When Dave comes online, he will reconnect to Carol, try to resync // the channel, but it will already be closed. Carol should resend the // information Dave needs to sweep his funds. - if err := restartDave(); err != nil { - t.Fatalf("unable to restart Eve: %v", err) - } + require.NoError(ht, restartDave(), "unable to restart Eve") // Dave should sweep his funds. - _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err) - } + ht.Miner.AssertNumTxsInMempool(1) // Mine a block to confirm the sweep, and make sure Dave got his // balance back. - mineBlocks(t, net, 1, 1) - assertNodeNumChannels(t, dave, 0) - - err = wait.NoError(func() error { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - daveBalResp, err := dave.WalletBalance(ctxt, balReq) - if err != nil { - return fmt.Errorf("unable to get dave's balance: %v", - err) - } + ht.Miner.MineBlocksAndAssertNumTxes(1, 1) + ht.AssertNodeNumChannels(dave, 0) + err := wait.NoError(func() error { + daveBalResp := dave.RPC.WalletBalance() daveBalance := daveBalResp.ConfirmedBalance if daveBalance <= daveStartingBalance { return fmt.Errorf("expected dave to have balance "+ - "above %d, instead had %v", daveStartingBalance, + "above %d, intead had %v", daveStartingBalance, daveBalance) } return nil }, defaultTimeout) - if err != nil { - t.Fatalf("%v", err) - } + require.NoError(ht, err, "timeout while checking dave's balance") } // createLegacyRevocationChannel creates a single channel using the legacy diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index e185fc88d..a08295122 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -223,10 +223,6 @@ var allTestCases = []*testCase{ name: "revoked uncooperative close retribution altruist watchtower", test: testRevokedCloseRetributionAltruistWatchtower, }, - { - name: "data loss protection", - test: testDataLossProtection, - }, { name: "query routes", test: testQueryRoutes,