From afccca59c40eac5f4495a922c54be8c70d0ff612 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 12 Jul 2018 11:02:54 +0200 Subject: [PATCH] lnd_test: add testDataLossProtection This commit adds the integration test testDataLossProtection, that ensures that when a node loses state, the channel counterparty will force close the channel, and they both can recover their funds. --- lnd_test.go | 332 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 330 insertions(+), 2 deletions(-) diff --git a/lnd_test.go b/lnd_test.go index d1f76b5b..592cd1cd 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -6046,19 +6046,343 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, assertNodeNumChannels(t, ctxb, dave, 0) } +// assertNumPendingChannels checks that a PendingChannels response from the +// node reports the expected number of pending channels. +func assertNumPendingChannels(t *harnessTest, node *lntest.HarnessNode, + expWaitingClose, expPendingForceClose int) { + ctxb := context.Background() + + var predErr error + err := lntest.WaitPredicate(func() bool { + pendingChansRequest := &lnrpc.PendingChannelsRequest{} + pendingChanResp, err := node.PendingChannels(ctxb, + pendingChansRequest) + if err != nil { + predErr = fmt.Errorf("unable to query for pending "+ + "channels: %v", err) + return false + } + n := len(pendingChanResp.WaitingCloseChannels) + if n != expWaitingClose { + predErr = fmt.Errorf("Expected to find %d channels "+ + "waiting close, found %d", expWaitingClose, n) + return false + } + n = len(pendingChanResp.PendingForceClosingChannels) + if n != expPendingForceClose { + predErr = fmt.Errorf("expected to find %d channel "+ + "pending force close, found %d", expPendingForceClose, n) + return false + } + return true + }, time.Second*15) + if err != nil { + t.Fatalf("%v", predErr) + } +} + +// 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 +// oppurtunity to sweep its output. +func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + const ( + timeout = time.Duration(time.Second * 10) + chanAmt = maxBtcFundingAmount + paymentAmt = 10000 + numInvoices = 6 + defaultCSV = uint32(4) + ) + + // 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. + carol, err := net.NewNode("Carol", []string{"--nolisten"}) + if err != nil { + t.Fatalf("unable to create new carol node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + // Dave will be the party losing his state. + dave, err := net.NewNode("Dave", nil) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, dave) + + // We must let Dave communicate with Carol before they are able to open + // channel, so we connect them. + if err := net.ConnectNodes(ctxb, carol, dave); err != nil { + t.Fatalf("unable to connect dave to carol: %v", err) + } + + // Before we make a channel, we'll load up Carol with some coins sent + // directly from the miner. + err = net.SendCoins(ctxb, btcutil.SatoshiPerBitcoin, carol) + if err != nil { + t.Fatalf("unable to send coins to carol: %v", err) + } + + // We'll first open up a channel between them with a 0.5 BTC value. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPoint := openChannelAndAssert( + ctxt, t, net, carol, dave, chanAmt, 0, false, + ) + + // We a´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{} + carolBalResp, err := carol.WalletBalance(ctxb, balReq) + if err != nil { + t.Fatalf("unable to get carol's balance: %v", err) + } + carolStartingBalance := carolBalResp.ConfirmedBalance + + daveBalResp, err := dave.WalletBalance(ctxb, balReq) + if err != nil { + t.Fatalf("unable to get dave's balance: %v", err) + } + daveStartingBalance := daveBalResp.ConfirmedBalance + + // With the channel open, we'll create a few invoices for Dave that + // Carol will pay to in order to advance the state of the channel. + // TODO(halseth): have dangling HTLCs on the commitment, able to + // retrive funds? + davePayReqs := make([]string, numInvoices) + for i := 0; i < numInvoices; i++ { + preimage := bytes.Repeat([]byte{byte(17 - i)}, 32) + invoice := &lnrpc.Invoice{ + Memo: "testing", + RPreimage: preimage, + Value: paymentAmt, + } + resp, err := dave.AddInvoice(ctxb, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + davePayReqs[i] = resp.PaymentRequest + } + + // As we'll be querying the state of Dave's channels frequently we'll + // create a closure helper function for the purpose. + getDaveChanInfo := func() (*lnrpc.Channel, error) { + req := &lnrpc.ListChannelsRequest{} + daveChannelInfo, err := dave.ListChannels(ctxb, req) + if err != nil { + return nil, err + } + if len(daveChannelInfo.Channels) != 1 { + t.Fatalf("dave should only have a single channel, "+ + "instead he has %v", + len(daveChannelInfo.Channels)) + } + + return daveChannelInfo.Channels[0], nil + } + + // Wait for Carol to receive the channel edge from the funding manager. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("carol didn't see the carol->dave channel before "+ + "timeout: %v", err) + } + + // Send payments from Carol to Dave using 3 of Dave's payment hashes + // generated above. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = completePaymentRequests(ctxt, carol, davePayReqs[:numInvoices/2], + true) + if err != nil { + t.Fatalf("unable to send payments: %v", err) + } + + // Next query for Dave's channel state, as we sent 3 payments of 10k + // satoshis each, Dave should now see his balance as being 30k satoshis. + var daveChan *lnrpc.Channel + var predErr error + err = lntest.WaitPredicate(func() bool { + bChan, err := getDaveChanInfo() + if err != nil { + t.Fatalf("unable to get dave's channel info: %v", err) + } + if bChan.LocalBalance != 30000 { + predErr = fmt.Errorf("dave's balance is incorrect, "+ + "got %v, expected %v", bChan.LocalBalance, + 30000) + return false + } + + daveChan = bChan + return true + }, time.Second*15) + if err != nil { + t.Fatalf("%v", predErr) + } + + // Grab Dave's current commitment height (update number), we'll later + // revert him to this state after additional updates to revoke this + // state. + daveStateNumPreCopy := daveChan.NumUpdates + + // Create a temporary file to house Dave's database state at this + // particular point in history. + daveTempDbPath, err := ioutil.TempDir("", "dave-past-state") + if err != nil { + t.Fatalf("unable to create temp db folder: %v", err) + } + daveTempDbFile := filepath.Join(daveTempDbPath, "channel.db") + defer os.Remove(daveTempDbPath) + + // With the temporary file created, copy Dave's current state into the + // temporary file we created above. Later after more updates, we'll + // restore this state. + if err := copyFile(daveTempDbFile, dave.DBPath()); err != nil { + t.Fatalf("unable to copy database files: %v", err) + } + + // Finally, send payments from Carol to Dave, consuming Dave's remaining + // payment hashes. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err = completePaymentRequests(ctxt, carol, davePayReqs[numInvoices/2:], + true) + if err != nil { + t.Fatalf("unable to send payments: %v", err) + } + + daveChan, err = getDaveChanInfo() + if err != nil { + t.Fatalf("unable to get dave chan info: %v", err) + } + + // Now we shutdown Dave, 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 Dave to travel back in time + // within the channel's history. + if err = net.RestartNode(dave, func() error { + return os.Rename(daveTempDbFile, dave.DBPath()) + }); err != nil { + t.Fatalf("unable to restart node: %v", err) + } + + // Now query for Dave's channel state, it should show that he's at a + // state number in the past, not the *latest* state. + daveChan, err = getDaveChanInfo() + if err != nil { + t.Fatalf("unable to get dave chan info: %v", err) + } + if daveChan.NumUpdates != daveStateNumPreCopy { + t.Fatalf("db copy failed: %v", daveChan.NumUpdates) + } + assertNodeNumChannels(t, ctxb, dave, 1) + + // Upon reconnection, the nodes should detect that Dave is out of sync. + if err := net.ConnectNodes(ctxb, carol, dave); err != nil { + t.Fatalf("unable to connect dave to carol: %v", err) + } + + // Carol should force close the channel using her latest commitment. + forceClose, err := waitForTxInMempool(net.Miner.Node, 5*time.Second) + if err != nil { + t.Fatalf("unable to find Carol's force close tx in mempool: %v", + err) + } + + // 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. + if err := net.RestartNode(dave, nil); err != nil { + t.Fatalf("Node restart failed: %v", err) + } + + // Generate a single block, which should confirm the closing tx. + block := mineBlocks(t, net, 1)[0] + assertTxInBlock(t, block, forceClose) + + // Dave should sweep his funds immediately, as they are not timelocked. + daveSweep, err := waitForTxInMempool(net.Miner.Node, 15*time.Second) + if err != nil { + t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err) + } + + // 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 whe must wait + // before she can sweep her outputs. + assertNumPendingChannels(t, carol, 0, 1) + + block = mineBlocks(t, net, 1)[0] + assertTxInBlock(t, block, daveSweep) + + // Now Dave should consider the channel fully closed. + assertNumPendingChannels(t, dave, 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. + daveBalResp, err = dave.WalletBalance(ctxb, balReq) + if err != nil { + t.Fatalf("unable to get dave's balance: %v", err) + } + daveBalance := daveBalResp.ConfirmedBalance + if daveBalance <= daveStartingBalance { + t.Fatalf("expected dave to have balance above %d, intead had %v", + daveStartingBalance, daveBalance) + } + + // After the Carol's output matures, she should also reclaim her funds. + mineBlocks(t, net, defaultCSV-1) + carolSweep, err := waitForTxInMempool(net.Miner.Node, 5*time.Second) + if err != nil { + t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err) + } + block = mineBlocks(t, net, 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. + carolBalResp, err = carol.WalletBalance(ctxb, 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, ctxb, dave, 0) + assertNodeNumChannels(t, ctxb, carol, 0) +} + // assertNodeNumChannels polls the provided node's list channels rpc until it // reaches the desired number of total channels. func assertNodeNumChannels(t *harnessTest, ctxb context.Context, node *lntest.HarnessNode, numChannels int) { - // Poll alice for her list of channels. + // Poll node for its list of channels. req := &lnrpc.ListChannelsRequest{} var predErr error pred := func() bool { chanInfo, err := node.ListChannels(ctxb, req) if err != nil { - predErr = fmt.Errorf("unable to query for alice's "+ + predErr = fmt.Errorf("unable to query for node's "+ "channels: %v", err) return false } @@ -10814,6 +11138,10 @@ var testsCases = []*testCase{ name: "revoked uncooperative close retribution remote hodl", test: testRevokedCloseRetributionRemoteHodl, }, + { + name: "data loss protection", + test: testDataLossProtection, + }, { name: "query routes", test: testQueryRoutes,