diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index 7a3995990..8e8456588 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntemp" @@ -1162,6 +1163,305 @@ func testChanRestoreScenario(ht *lntemp.HarnessTest, ) } +// testDataLossProtection tests that if one of the nodes in a channel +// 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() + const ( + chanAmt = funding.MaxBtcFundingAmount + paymentAmt = 10000 + numInvoices = 6 + ) + + // Carol will be the up-to-date party. We set --nolisten to ensure Dave + // won't be able to connect to her and trigger the channel data + // 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) + + // Dave will be the party losing his state. + dave := net.NewNode(t.t, "Dave", nil) + defer shutdownAndAssert(net, t, dave) + + // 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) + + // 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) { + + // 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) + + // We'll first open up a channel between them with a 0.5 BTC + // value. + chanPoint := openChannelAndAssert( + t, net, carol, node, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + // With the channel open, we'll create a few invoices for the + // node that Carol will pay to in order to advance the state of + // 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) + } + + // 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) + } + + // 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) + } + + // Grab the current commitment height (update number), we'll + // later revert him to this state after additional updates to + // revoke this state. + stateNumPreCopy := nodeChan.NumUpdates + + // 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) + } + + // Reconnect the peers after the restart that was needed for the db + // backup. + net.EnsureConnected(t.t, 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) + } + + // 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) + } + + // Make sure the channel is still there from the PoV of the + // node. + assertNodeNumChannels(t, 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) + } + + 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) + } + + restart, err := net.SuspendNode(node) + if err != nil { + t.Fatalf("unable to suspend node: %v", err) + } + + return restart, chanPoint, balResp.ConfirmedBalance, nil + } + + // 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) + } + + // 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) + } + carolStartingBalance := carolBalResp.ConfirmedBalance + + // Restart Dave to trigger a channel resync. + if err := restartDave(); err != nil { + t.Fatalf("unable to restart dave: %v", err) + } + + // 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, + ) + + // As a second part of this test, we will test the scenario where a + // channel is closed while Dave is offline, loses his state and comes + // back online. In this case the node should attempt to resync the + // channel, and the peer should resend a channel sync message for the + // 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) + } + + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + carolBalResp, err = carol.WalletBalance(ctxt, balReq) + if err != nil { + t.Fatalf("unable to get carol's balance: %v", err) + } + 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) + + // 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) + } + carolBalance := carolBalResp.ConfirmedBalance + if carolBalance <= carolStartingBalance { + t.Fatalf("expected carol to have balance above %d, "+ + "instead had %v", carolStartingBalance, + carolBalance) + } + + assertNodeNumChannels(t, 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) + } + + // 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) + } + + // 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) + } + + daveBalance := daveBalResp.ConfirmedBalance + if daveBalance <= daveStartingBalance { + return fmt.Errorf("expected dave to have balance "+ + "above %d, instead had %v", daveStartingBalance, + daveBalance) + } + + return nil + }, defaultTimeout) + if err != nil { + t.Fatalf("%v", err) + } +} + // createLegacyRevocationChannel creates a single channel using the legacy // revocation producer format by using PSBT to signal a special pending channel // ID. diff --git a/lntest/itest/lnd_misc_test.go b/lntest/itest/lnd_misc_test.go index 07ea01548..99472f148 100644 --- a/lntest/itest/lnd_misc_test.go +++ b/lntest/itest/lnd_misc_test.go @@ -821,305 +821,6 @@ func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(t, net, net.Alice, persistentChanPoint, false) } -// testDataLossProtection tests that if one of the nodes in a channel -// 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() - const ( - chanAmt = funding.MaxBtcFundingAmount - paymentAmt = 10000 - numInvoices = 6 - ) - - // Carol will be the up-to-date party. We set --nolisten to ensure Dave - // won't be able to connect to her and trigger the channel data - // 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) - - // Dave will be the party losing his state. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) - - // 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) - - // 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) { - - // 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) - - // We'll first open up a channel between them with a 0.5 BTC - // value. - chanPoint := openChannelAndAssert( - t, net, carol, node, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - - // With the channel open, we'll create a few invoices for the - // node that Carol will pay to in order to advance the state of - // 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) - } - - // 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) - } - - // 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) - } - - // Grab the current commitment height (update number), we'll - // later revert him to this state after additional updates to - // revoke this state. - stateNumPreCopy := nodeChan.NumUpdates - - // 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) - } - - // Reconnect the peers after the restart that was needed for the db - // backup. - net.EnsureConnected(t.t, 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) - } - - // 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) - } - - // Make sure the channel is still there from the PoV of the - // node. - assertNodeNumChannels(t, 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) - } - - 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) - } - - restart, err := net.SuspendNode(node) - if err != nil { - t.Fatalf("unable to suspend node: %v", err) - } - - return restart, chanPoint, balResp.ConfirmedBalance, nil - } - - // 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) - } - - // 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) - } - carolStartingBalance := carolBalResp.ConfirmedBalance - - // Restart Dave to trigger a channel resync. - if err := restartDave(); err != nil { - t.Fatalf("unable to restart dave: %v", err) - } - - // 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, - ) - - // As a second part of this test, we will test the scenario where a - // channel is closed while Dave is offline, loses his state and comes - // back online. In this case the node should attempt to resync the - // channel, and the peer should resend a channel sync message for the - // 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) - } - - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolBalResp, err = carol.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get carol's balance: %v", err) - } - 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) - - // 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) - } - carolBalance := carolBalResp.ConfirmedBalance - if carolBalance <= carolStartingBalance { - t.Fatalf("expected carol to have balance above %d, "+ - "instead had %v", carolStartingBalance, - carolBalance) - } - - assertNodeNumChannels(t, 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) - } - - // 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) - } - - // 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) - } - - daveBalance := daveBalResp.ConfirmedBalance - if daveBalance <= daveStartingBalance { - return fmt.Errorf("expected dave to have balance "+ - "above %d, instead had %v", daveStartingBalance, - daveBalance) - } - - return nil - }, defaultTimeout) - if err != nil { - t.Fatalf("%v", err) - } -} - // testRejectHTLC tests that a node can be created with the flag --rejecthtlc. // This means that the node will reject all forwarded HTLCs but can still // accept direct HTLCs as well as send HTLCs.