From 0655a1bcfb13a83bca421253ba49a488139958a2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 12 Aug 2020 16:06:34 +0200 Subject: [PATCH] itest: move channel backup tests to own file --- lntest/itest/lnd_channel_backup_test.go | 1022 +++++++++++++++++++++++ lntest/itest/lnd_test.go | 1002 ---------------------- 2 files changed, 1022 insertions(+), 1002 deletions(-) create mode 100644 lntest/itest/lnd_channel_backup_test.go diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go new file mode 100644 index 00000000..29d81407 --- /dev/null +++ b/lntest/itest/lnd_channel_backup_test.go @@ -0,0 +1,1022 @@ +// +build rpctest + +package itest + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/wait" +) + +// testChannelBackupRestore tests that we're able to recover from, and initiate +// the DLP protocol via: the RPC restore command, restoring on unlock, and +// restoring from initial wallet creation. We'll also alternate between +// restoring form the on disk file, and restoring from the exported RPC command +// as well. +func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { + password := []byte("El Psy Kongroo") + + ctxb := context.Background() + + var testCases = []chanRestoreTestCase{ + // Restore from backups obtained via the RPC interface. Dave + // was the initiator, of the non-advertised channel. + { + name: "restore from RPC backup", + channelsUpdated: false, + initiator: true, + private: false, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // For this restoration method, we'll grab the + // current multi-channel backup from the old + // node, and use it to restore a new node + // within the closure. + req := &lnrpc.ChanBackupExportRequest{} + chanBackup, err := oldNode.ExportAllChannelBackups( + ctxb, req, + ) + if err != nil { + return nil, fmt.Errorf("unable to obtain "+ + "channel backup: %v", err) + } + + multi := chanBackup.MultiChanBackup.MultiChanBackup + + // In our nodeRestorer function, we'll restore + // the node from seed, then manually recover + // the channel backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, + + // Restore the backup from the on-disk file, using the RPC + // interface. + { + name: "restore from backup file", + initiator: true, + private: false, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channels.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that will restore + // using the on-disk channels.backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, + + // Restore the backup as part of node initialization with the + // prior mnemonic and new backup seed. + { + name: "restore during creation", + initiator: true, + private: false, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // First, fetch the current backup state as is, + // to obtain our latest Multi. + chanBackup, err := oldNode.ExportAllChannelBackups( + ctxb, &lnrpc.ChanBackupExportRequest{}, + ) + if err != nil { + return nil, fmt.Errorf("unable to obtain "+ + "channel backup: %v", err) + } + backupSnapshot := &lnrpc.ChanBackupSnapshot{ + MultiChanBackup: chanBackup.MultiChanBackup, + } + + // Create a new nodeRestorer that will restore + // the node using the Multi backup we just + // obtained above. + return func() (*lntest.HarnessNode, error) { + return net.RestoreNodeWithSeed( + "dave", nil, password, + mnemonic, 1000, backupSnapshot, + ) + }, nil + }, + }, + + // Restore the backup once the node has already been + // re-created, using the Unlock call. + { + name: "restore during unlock", + initiator: true, + private: false, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // First, fetch the current backup state as is, + // to obtain our latest Multi. + chanBackup, err := oldNode.ExportAllChannelBackups( + ctxb, &lnrpc.ChanBackupExportRequest{}, + ) + if err != nil { + return nil, fmt.Errorf("unable to obtain "+ + "channel backup: %v", err) + } + backupSnapshot := &lnrpc.ChanBackupSnapshot{ + MultiChanBackup: chanBackup.MultiChanBackup, + } + + // Create a new nodeRestorer that will restore + // the node with its seed, but no channel + // backup, shutdown this initialized node, then + // restart it again using Unlock. + return func() (*lntest.HarnessNode, error) { + newNode, err := net.RestoreNodeWithSeed( + "dave", nil, password, + mnemonic, 1000, nil, + ) + if err != nil { + return nil, err + } + + err = net.RestartNode( + newNode, nil, backupSnapshot, + ) + if err != nil { + return nil, err + } + + return newNode, nil + }, nil + }, + }, + + // Restore the backup from the on-disk file a second time to + // make sure imports can be canceled and later resumed. + { + name: "restore from backup file twice", + initiator: true, + private: false, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channels.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that will restore + // using the on-disk channels.backup. + backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ + MultiChanBackup: multi, + } + + ctxb := context.Background() + + return func() (*lntest.HarnessNode, error) { + newNode, err := net.RestoreNodeWithSeed( + "dave", nil, password, mnemonic, + 1000, nil, + ) + if err != nil { + return nil, fmt.Errorf("unable to "+ + "restore node: %v", err) + } + + _, err = newNode.RestoreChannelBackups( + ctxb, + &lnrpc.RestoreChanBackupRequest{ + Backup: backup, + }, + ) + if err != nil { + return nil, fmt.Errorf("unable "+ + "to restore backups: %v", + err) + } + + _, err = newNode.RestoreChannelBackups( + ctxb, + &lnrpc.RestoreChanBackupRequest{ + Backup: backup, + }, + ) + if err != nil { + return nil, fmt.Errorf("unable "+ + "to restore backups the"+ + "second time: %v", + err) + } + + return newNode, nil + }, nil + }, + }, + + // Use the channel backup file that contains an unconfirmed + // channel and make sure recovery works as well. + { + name: "restore unconfirmed channel file", + channelsUpdated: false, + initiator: true, + private: false, + unconfirmed: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channels.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Let's assume time passes, the channel + // confirms in the meantime but for some reason + // the backup we made while it was still + // unconfirmed is the only backup we have. We + // should still be able to restore it. To + // simulate time passing, we mine some blocks + // to get the channel confirmed _after_ we saved + // the backup. + mineBlocks(t, net, 6, 1) + + // In our nodeRestorer function, we'll restore + // the node from seed, then manually recover + // the channel backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, + + // Create a backup using RPC that contains an unconfirmed + // channel and make sure recovery works as well. + { + name: "restore unconfirmed channel RPC", + channelsUpdated: false, + initiator: true, + private: false, + unconfirmed: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // For this restoration method, we'll grab the + // current multi-channel backup from the old + // node. The channel should be included, even if + // it is not confirmed yet. + req := &lnrpc.ChanBackupExportRequest{} + chanBackup, err := oldNode.ExportAllChannelBackups( + ctxb, req, + ) + if err != nil { + return nil, fmt.Errorf("unable to obtain "+ + "channel backup: %v", err) + } + chanPoints := chanBackup.MultiChanBackup.ChanPoints + if len(chanPoints) == 0 { + return nil, fmt.Errorf("unconfirmed " + + "channel not included in backup") + } + + // Let's assume time passes, the channel + // confirms in the meantime but for some reason + // the backup we made while it was still + // unconfirmed is the only backup we have. We + // should still be able to restore it. To + // simulate time passing, we mine some blocks + // to get the channel confirmed _after_ we saved + // the backup. + mineBlocks(t, net, 6, 1) + + // In our nodeRestorer function, we'll restore + // the node from seed, then manually recover + // the channel backup. + multi := chanBackup.MultiChanBackup.MultiChanBackup + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, + + // Restore the backup from the on-disk file, using the RPC + // interface, for anchor commitment channels. + { + name: "restore from backup file anchors", + initiator: true, + private: false, + anchorCommit: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channels.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that will restore + // using the on-disk channels.backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + ) + }, + }, + } + + // TODO(roasbeef): online vs offline close? + + // TODO(roasbeef): need to re-trigger the on-disk file once the node + // ann is updated? + + for _, testCase := range testCases { + success := t.t.Run(testCase.name, func(t *testing.T) { + h := newHarnessTest(t, net) + testChanRestoreScenario(h, net, &testCase, password) + }) + if !success { + break + } + } +} + +// testChannelBackupUpdates tests that both the streaming channel update RPC, +// and the on-disk channels.backup are updated each time a channel is +// opened/closed. +func testChannelBackupUpdates(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + // First, we'll make a temp directory that we'll use to store our + // backup file, so we can check in on it during the test easily. + backupDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("unable to create backup dir: %v", err) + } + defer os.RemoveAll(backupDir) + + // First, we'll create a new node, Carol. We'll also create a temporary + // file that Carol will use to store her channel backups. + backupFilePath := filepath.Join( + backupDir, chanbackup.DefaultBackupFileName, + ) + carolArgs := fmt.Sprintf("--backupfilepath=%v", backupFilePath) + carol, err := net.NewNode("carol", []string{carolArgs}) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + // Next, we'll register for streaming notifications for changes to the + // backup file. + backupStream, err := carol.SubscribeChannelBackups( + ctxb, &lnrpc.ChannelBackupSubscription{}, + ) + if err != nil { + t.Fatalf("unable to create backup stream: %v", err) + } + + // We'll use this goroutine to proxy any updates to a channel we can + // easily use below. + var wg sync.WaitGroup + backupUpdates := make(chan *lnrpc.ChanBackupSnapshot) + streamErr := make(chan error) + streamQuit := make(chan struct{}) + + wg.Add(1) + go func() { + defer wg.Done() + for { + snapshot, err := backupStream.Recv() + if err != nil { + select { + case streamErr <- err: + case <-streamQuit: + return + } + } + + select { + case backupUpdates <- snapshot: + case <-streamQuit: + return + } + } + }() + defer close(streamQuit) + + // With Carol up, we'll now connect her to Alice, and open a channel + // between them. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil { + t.Fatalf("unable to connect carol to alice: %v", err) + } + + // Next, we'll open two channels between Alice and Carol back to back. + var chanPoints []*lnrpc.ChannelPoint + numChans := 2 + chanAmt := btcutil.Amount(1000000) + for i := 0; i < numChans; i++ { + ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) + chanPoint := openChannelAndAssert( + ctxt, t, net, net.Alice, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + chanPoints = append(chanPoints, chanPoint) + } + + // Using this helper function, we'll maintain a pointer to the latest + // channel backup so we can compare it to the on disk state. + var currentBackup *lnrpc.ChanBackupSnapshot + assertBackupNtfns := func(numNtfns int) { + for i := 0; i < numNtfns; i++ { + select { + case err := <-streamErr: + t.Fatalf("error with backup stream: %v", err) + + case currentBackup = <-backupUpdates: + + case <-time.After(time.Second * 5): + t.Fatalf("didn't receive channel backup "+ + "notification %v", i+1) + } + } + } + + // assertBackupFileState is a helper function that we'll use to compare + // the on disk back up file to our currentBackup pointer above. + assertBackupFileState := func() { + err := wait.NoError(func() error { + packedBackup, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return fmt.Errorf("unable to read backup "+ + "file: %v", err) + } + + // As each back up file will be encrypted with a fresh + // nonce, we can't compare them directly, so instead + // we'll compare the length which is a proxy for the + // number of channels that the multi-backup contains. + rawBackup := currentBackup.MultiChanBackup.MultiChanBackup + if len(rawBackup) != len(packedBackup) { + return fmt.Errorf("backup files don't match: "+ + "expected %x got %x", rawBackup, packedBackup) + } + + // Additionally, we'll assert that both backups up + // returned are valid. + for i, backup := range [][]byte{rawBackup, packedBackup} { + snapshot := &lnrpc.ChanBackupSnapshot{ + MultiChanBackup: &lnrpc.MultiChanBackup{ + MultiChanBackup: backup, + }, + } + _, err := carol.VerifyChanBackup(ctxb, snapshot) + if err != nil { + return fmt.Errorf("unable to verify "+ + "backup #%d: %v", i, err) + } + } + + return nil + }, time.Second*15) + if err != nil { + t.Fatalf("backup state invalid: %v", err) + } + } + + // As these two channels were just opened, we should've got two times + // the pending and open notifications for channel backups. + assertBackupNtfns(2 * 2) + + // The on disk file should also exactly match the latest backup that we + // have. + assertBackupFileState() + + // Next, we'll close the channels one by one. After each channel + // closure, we should get a notification, and the on-disk state should + // match this state as well. + for i := 0; i < numChans; i++ { + // To ensure force closes also trigger an update, we'll force + // close half of the channels. + forceClose := i%2 == 0 + + chanPoint := chanPoints[i] + + ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) + closeChannelAndAssert( + ctxt, t, net, net.Alice, chanPoint, forceClose, + ) + + // We should get a single notification after closing, and the + // on-disk state should match this latest notifications. + assertBackupNtfns(1) + assertBackupFileState() + + // If we force closed the channel, then we'll mine enough + // blocks to ensure all outputs have been swept. + if forceClose { + cleanupForceClose(t, net, net.Alice, chanPoint) + } + } +} + +// testExportChannelBackup tests that we're able to properly export either a +// targeted channel's backup, or export backups of all the currents open +// channels. +func testExportChannelBackup(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + // First, we'll create our primary test node: Carol. We'll use Carol to + // open channels and also export backups that we'll examine throughout + // the test. + carol, err := net.NewNode("carol", nil) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + // With Carol up, we'll now connect her to Alice, and open a channel + // between them. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil { + t.Fatalf("unable to connect carol to alice: %v", err) + } + + // Next, we'll open two channels between Alice and Carol back to back. + var chanPoints []*lnrpc.ChannelPoint + numChans := 2 + chanAmt := btcutil.Amount(1000000) + for i := 0; i < numChans; i++ { + ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) + chanPoint := openChannelAndAssert( + ctxt, t, net, net.Alice, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + chanPoints = append(chanPoints, chanPoint) + } + + // Now that the channels are open, we should be able to fetch the + // backups of each of the channels. + for _, chanPoint := range chanPoints { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + req := &lnrpc.ExportChannelBackupRequest{ + ChanPoint: chanPoint, + } + chanBackup, err := carol.ExportChannelBackup(ctxt, req) + if err != nil { + t.Fatalf("unable to fetch backup for channel %v: %v", + chanPoint, err) + } + + // The returned backup should be full populated. Since it's + // encrypted, we can't assert any more than that atm. + if len(chanBackup.ChanBackup) == 0 { + t.Fatalf("obtained empty backup for channel: %v", chanPoint) + } + + // The specified chanPoint in the response should match our + // requested chanPoint. + if chanBackup.ChanPoint.String() != chanPoint.String() { + t.Fatalf("chanPoint mismatched: expected %v, got %v", + chanPoint.String(), + chanBackup.ChanPoint.String()) + } + } + + // Before we proceed, we'll make two utility methods we'll use below + // for our primary assertions. + assertNumSingleBackups := func(numSingles int) { + err := wait.NoError(func() error { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + req := &lnrpc.ChanBackupExportRequest{} + chanSnapshot, err := carol.ExportAllChannelBackups( + ctxt, req, + ) + if err != nil { + return fmt.Errorf("unable to export channel "+ + "backup: %v", err) + } + + if chanSnapshot.SingleChanBackups == nil { + return fmt.Errorf("single chan backups not " + + "populated") + } + + backups := chanSnapshot.SingleChanBackups.ChanBackups + if len(backups) != numSingles { + return fmt.Errorf("expected %v singles, "+ + "got %v", len(backups), numSingles) + } + + return nil + }, defaultTimeout) + if err != nil { + t.Fatalf(err.Error()) + } + } + assertMultiBackupFound := func() func(bool, map[wire.OutPoint]struct{}) { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + req := &lnrpc.ChanBackupExportRequest{} + chanSnapshot, err := carol.ExportAllChannelBackups(ctxt, req) + if err != nil { + t.Fatalf("unable to export channel backup: %v", err) + } + + return func(found bool, chanPoints map[wire.OutPoint]struct{}) { + switch { + case found && chanSnapshot.MultiChanBackup == nil: + t.Fatalf("multi-backup not present") + + case !found && chanSnapshot.MultiChanBackup != nil && + (len(chanSnapshot.MultiChanBackup.MultiChanBackup) != + chanbackup.NilMultiSizePacked): + + t.Fatalf("found multi-backup when non should " + + "be found") + } + + if !found { + return + } + + backedUpChans := chanSnapshot.MultiChanBackup.ChanPoints + if len(chanPoints) != len(backedUpChans) { + t.Fatalf("expected %v chans got %v", len(chanPoints), + len(backedUpChans)) + } + + for _, chanPoint := range backedUpChans { + wirePoint := rpcPointToWirePoint(t, chanPoint) + if _, ok := chanPoints[wirePoint]; !ok { + t.Fatalf("unexpected backup: %v", wirePoint) + } + } + } + } + + chans := make(map[wire.OutPoint]struct{}) + for _, chanPoint := range chanPoints { + chans[rpcPointToWirePoint(t, chanPoint)] = struct{}{} + } + + // We should have exactly two single channel backups contained, and we + // should also have a multi-channel backup. + assertNumSingleBackups(2) + assertMultiBackupFound()(true, chans) + + // We'll now close each channel on by one. After we close a channel, we + // shouldn't be able to find that channel as a backup still. We should + // also have one less single written to disk. + for i, chanPoint := range chanPoints { + ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) + closeChannelAndAssert( + ctxt, t, net, net.Alice, chanPoint, false, + ) + + assertNumSingleBackups(len(chanPoints) - i - 1) + + delete(chans, rpcPointToWirePoint(t, chanPoint)) + assertMultiBackupFound()(true, chans) + } + + // At this point we shouldn't have any single or multi-chan backups at + // all. + assertNumSingleBackups(0) + assertMultiBackupFound()(false, nil) +} + +// nodeRestorer is a function closure that allows each chanRestoreTestCase to +// control exactly *how* the prior node is restored. This might be using an +// backup obtained over RPC, or the file system, etc. +type nodeRestorer func() (*lntest.HarnessNode, error) + +// chanRestoreTestCase describes a test case for an end to end SCB restoration +// work flow. One node will start from scratch using an existing SCB. At the +// end of the est, both nodes should be made whole via the DLP protocol. +type chanRestoreTestCase struct { + // name is the name of the target test case. + name string + + // channelsUpdated is false then this means that no updates + // have taken place within the channel before restore. + // Otherwise, HTLCs will be settled between the two parties + // before restoration modifying the balance beyond the initial + // allocation. + channelsUpdated bool + + // initiator signals if Dave should be the one that opens the + // channel to Alice, or if it should be the other way around. + initiator bool + + // private signals if the channel from Dave to Carol should be + // private or not. + private bool + + // unconfirmed signals if the channel from Dave to Carol should be + // confirmed or not. + unconfirmed bool + + // anchorCommit is true, then the new anchor commitment type will be + // used for the channels created in the test. + anchorCommit bool + + // restoreMethod takes an old node, then returns a function + // closure that'll return the same node, but with its state + // restored via a custom method. We use this to abstract away + // _how_ a node is restored from our assertions once the node + // has been fully restored itself. + restoreMethod func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) +} + +// testChanRestoreScenario executes a chanRestoreTestCase from end to end, +// ensuring that after Dave restores his channel state according to the +// testCase, the DLP protocol is executed properly and both nodes are made +// whole. +func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, + testCase *chanRestoreTestCase, password []byte) { + + const ( + chanAmt = btcutil.Amount(10000000) + pushAmt = btcutil.Amount(5000000) + ) + + ctxb := context.Background() + + var nodeArgs []string + if testCase.anchorCommit { + nodeArgs = commitTypeAnchors.Args() + } + + // First, we'll create a brand new node we'll use within the test. If + // we have a custom backup file specified, then we'll also create that + // for use. + dave, mnemonic, err := net.NewNodeWithSeed( + "dave", nodeArgs, password, + ) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + // Defer to a closure instead of to shutdownAndAssert due to the value + // of 'dave' changing throughout the test. + defer func() { + shutdownAndAssert(net, t, dave) + }() + carol, err := net.NewNode("carol", nodeArgs) + if err != nil { + t.Fatalf("unable to make new node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + // Now that our new nodes are created, we'll give them some coins for + // channel opening and anchor sweeping. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol) + if err != nil { + t.Fatalf("unable to send coins to dave: %v", err) + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, dave) + if err != nil { + t.Fatalf("unable to send coins to dave: %v", err) + } + + var from, to *lntest.HarnessNode + if testCase.initiator { + from, to = dave, carol + } else { + from, to = carol, dave + } + + // Next, we'll connect Dave to Carol, and open a new channel to her + // with a portion pushed. + if err := net.ConnectNodes(ctxt, dave, carol); err != nil { + t.Fatalf("unable to connect dave to carol: %v", err) + } + + // We will either open a confirmed or unconfirmed channel, depending on + // the requirements of the test case. + switch { + case testCase.unconfirmed: + ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) + _, err := net.OpenPendingChannel( + ctxt, from, to, chanAmt, pushAmt, + ) + if err != nil { + t.Fatalf("couldn't open pending channel: %v", err) + } + + // Give the pubsub some time to update the channel backup. + err = wait.NoError(func() error { + fi, err := os.Stat(dave.ChanBackupPath()) + if err != nil { + return err + } + if fi.Size() <= chanbackup.NilMultiSizePacked { + return fmt.Errorf("backup file empty") + } + return nil + }, defaultTimeout) + if err != nil { + t.Fatalf("channel backup not updated in time: %v", err) + } + + default: + ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) + chanPoint := openChannelAndAssert( + ctxt, t, net, from, to, + lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + Private: testCase.private, + }, + ) + + // Wait for both sides to see the opened channel. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = dave.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("dave didn't report channel: %v", err) + } + err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("carol didn't report channel: %v", err) + } + } + + // If both parties should start with existing channel updates, then + // we'll send+settle an HTLC between 'from' and 'to' now. + if testCase.channelsUpdated { + invoice := &lnrpc.Invoice{ + Memo: "testing", + Value: 10000, + } + invoiceResp, err := to.AddInvoice(ctxt, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + err = completePaymentRequests( + ctxt, from, from.RouterClient, + []string{invoiceResp.PaymentRequest}, true, + ) + if err != nil { + t.Fatalf("unable to complete payments: %v", err) + } + } + + // Before we start the recovery, we'll record the balances of both + // Carol and Dave to ensure they both sweep their coins at the end. + 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 + + daveBalance, err := dave.WalletBalance(ctxt, balReq) + if err != nil { + t.Fatalf("unable to get carol's balance: %v", err) + } + daveStartingBalance := daveBalance.ConfirmedBalance + + // At this point, we'll now execute the restore method to give us the + // new node we should attempt our assertions against. + backupFilePath := dave.ChanBackupPath() + restoredNodeFunc, err := testCase.restoreMethod( + dave, backupFilePath, mnemonic, + ) + if err != nil { + t.Fatalf("unable to prep node restoration: %v", err) + } + + // TODO(roasbeef): assert recovery state in channel + + // Now that we're able to make our restored now, we'll shutdown the old + // Dave node as we'll be storing it shortly below. + shutdownAndAssert(net, t, dave) + + // Next, we'll make a new Dave and start the bulk of our recovery + // workflow. + dave, err = restoredNodeFunc() + if err != nil { + t.Fatalf("unable to restore node: %v", err) + } + + // First ensure that the on-chain balance is restored. + err = wait.NoError(func() error { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + balReq := &lnrpc.WalletBalanceRequest{} + daveBalResp, err := dave.WalletBalance(ctxt, balReq) + if err != nil { + return err + } + + daveBal := daveBalResp.ConfirmedBalance + if daveBal <= 0 { + return fmt.Errorf("expected positive balance, had %v", + daveBal) + } + + return nil + }, defaultTimeout) + if err != nil { + t.Fatalf("On-chain balance not restored: %v", err) + } + + // Now that we have our new node up, we expect that it'll re-connect to + // Carol automatically based on the restored backup. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + err = net.EnsureConnected(ctxt, dave, carol) + if err != nil { + t.Fatalf("node didn't connect after recovery: %v", err) + } + + // TODO(roasbeef): move dave restarts? + + // Now we'll assert that both sides properly execute the DLP protocol. + // We grab their balances now to ensure that they're made whole at the + // end of the protocol. + assertDLPExecuted( + net, t, carol, carolStartingBalance, dave, daveStartingBalance, + testCase.anchorCommit, + ) +} + +// chanRestoreViaRPC is a helper test method that returns a nodeRestorer +// instance which will restore the target node from a password+seed, then +// trigger a SCB restore using the RPC interface. +func chanRestoreViaRPC(net *lntest.NetworkHarness, + password []byte, mnemonic []string, + multi []byte) (nodeRestorer, error) { + + backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ + MultiChanBackup: multi, + } + + ctxb := context.Background() + + return func() (*lntest.HarnessNode, error) { + newNode, err := net.RestoreNodeWithSeed( + "dave", nil, password, mnemonic, 1000, nil, + ) + if err != nil { + return nil, fmt.Errorf("unable to "+ + "restore node: %v", err) + } + + _, err = newNode.RestoreChannelBackups( + ctxb, &lnrpc.RestoreChanBackupRequest{ + Backup: backup, + }, + ) + if err != nil { + return nil, fmt.Errorf("unable "+ + "to restore backups: %v", err) + } + + return newNode, nil + }, nil +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 9c2bdd23..adb5949d 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -33,7 +33,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd" - "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lncfg" @@ -13350,1007 +13349,6 @@ func assertTxLabel(ctx context.Context, t *harnessTest, } } -// testChannelBackupUpdates tests that both the streaming channel update RPC, -// and the on-disk channels.backup are updated each time a channel is -// opened/closed. -func testChannelBackupUpdates(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - - // First, we'll make a temp directory that we'll use to store our - // backup file, so we can check in on it during the test easily. - backupDir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatalf("unable to create backup dir: %v", err) - } - defer os.RemoveAll(backupDir) - - // First, we'll create a new node, Carol. We'll also create a temporary - // file that Carol will use to store her channel backups. - backupFilePath := filepath.Join( - backupDir, chanbackup.DefaultBackupFileName, - ) - carolArgs := fmt.Sprintf("--backupfilepath=%v", backupFilePath) - carol, err := net.NewNode("carol", []string{carolArgs}) - if err != nil { - t.Fatalf("unable to create new node: %v", err) - } - defer shutdownAndAssert(net, t, carol) - - // Next, we'll register for streaming notifications for changes to the - // backup file. - backupStream, err := carol.SubscribeChannelBackups( - ctxb, &lnrpc.ChannelBackupSubscription{}, - ) - if err != nil { - t.Fatalf("unable to create backup stream: %v", err) - } - - // We'll use this goroutine to proxy any updates to a channel we can - // easily use below. - var wg sync.WaitGroup - backupUpdates := make(chan *lnrpc.ChanBackupSnapshot) - streamErr := make(chan error) - streamQuit := make(chan struct{}) - - wg.Add(1) - go func() { - defer wg.Done() - for { - snapshot, err := backupStream.Recv() - if err != nil { - select { - case streamErr <- err: - case <-streamQuit: - return - } - } - - select { - case backupUpdates <- snapshot: - case <-streamQuit: - return - } - } - }() - defer close(streamQuit) - - // With Carol up, we'll now connect her to Alice, and open a channel - // between them. - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil { - t.Fatalf("unable to connect carol to alice: %v", err) - } - - // Next, we'll open two channels between Alice and Carol back to back. - var chanPoints []*lnrpc.ChannelPoint - numChans := 2 - chanAmt := btcutil.Amount(1000000) - for i := 0; i < numChans; i++ { - ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) - chanPoint := openChannelAndAssert( - ctxt, t, net, net.Alice, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - - chanPoints = append(chanPoints, chanPoint) - } - - // Using this helper function, we'll maintain a pointer to the latest - // channel backup so we can compare it to the on disk state. - var currentBackup *lnrpc.ChanBackupSnapshot - assertBackupNtfns := func(numNtfns int) { - for i := 0; i < numNtfns; i++ { - select { - case err := <-streamErr: - t.Fatalf("error with backup stream: %v", err) - - case currentBackup = <-backupUpdates: - - case <-time.After(time.Second * 5): - t.Fatalf("didn't receive channel backup "+ - "notification %v", i+1) - } - } - } - - // assertBackupFileState is a helper function that we'll use to compare - // the on disk back up file to our currentBackup pointer above. - assertBackupFileState := func() { - err := wait.NoError(func() error { - packedBackup, err := ioutil.ReadFile(backupFilePath) - if err != nil { - return fmt.Errorf("unable to read backup "+ - "file: %v", err) - } - - // As each back up file will be encrypted with a fresh - // nonce, we can't compare them directly, so instead - // we'll compare the length which is a proxy for the - // number of channels that the multi-backup contains. - rawBackup := currentBackup.MultiChanBackup.MultiChanBackup - if len(rawBackup) != len(packedBackup) { - return fmt.Errorf("backup files don't match: "+ - "expected %x got %x", rawBackup, packedBackup) - } - - // Additionally, we'll assert that both backups up - // returned are valid. - for i, backup := range [][]byte{rawBackup, packedBackup} { - snapshot := &lnrpc.ChanBackupSnapshot{ - MultiChanBackup: &lnrpc.MultiChanBackup{ - MultiChanBackup: backup, - }, - } - _, err := carol.VerifyChanBackup(ctxb, snapshot) - if err != nil { - return fmt.Errorf("unable to verify "+ - "backup #%d: %v", i, err) - } - } - - return nil - }, time.Second*15) - if err != nil { - t.Fatalf("backup state invalid: %v", err) - } - } - - // As these two channels were just opened, we should've got two times - // the pending and open notifications for channel backups. - assertBackupNtfns(2 * 2) - - // The on disk file should also exactly match the latest backup that we - // have. - assertBackupFileState() - - // Next, we'll close the channels one by one. After each channel - // closure, we should get a notification, and the on-disk state should - // match this state as well. - for i := 0; i < numChans; i++ { - // To ensure force closes also trigger an update, we'll force - // close half of the channels. - forceClose := i%2 == 0 - - chanPoint := chanPoints[i] - - ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) - closeChannelAndAssert( - ctxt, t, net, net.Alice, chanPoint, forceClose, - ) - - // We should get a single notification after closing, and the - // on-disk state should match this latest notifications. - assertBackupNtfns(1) - assertBackupFileState() - - // If we force closed the channel, then we'll mine enough - // blocks to ensure all outputs have been swept. - if forceClose { - cleanupForceClose(t, net, net.Alice, chanPoint) - } - } -} - -// testExportChannelBackup tests that we're able to properly export either a -// targeted channel's backup, or export backups of all the currents open -// channels. -func testExportChannelBackup(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - - // First, we'll create our primary test node: Carol. We'll use Carol to - // open channels and also export backups that we'll examine throughout - // the test. - carol, err := net.NewNode("carol", nil) - if err != nil { - t.Fatalf("unable to create new node: %v", err) - } - defer shutdownAndAssert(net, t, carol) - - // With Carol up, we'll now connect her to Alice, and open a channel - // between them. - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil { - t.Fatalf("unable to connect carol to alice: %v", err) - } - - // Next, we'll open two channels between Alice and Carol back to back. - var chanPoints []*lnrpc.ChannelPoint - numChans := 2 - chanAmt := btcutil.Amount(1000000) - for i := 0; i < numChans; i++ { - ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) - chanPoint := openChannelAndAssert( - ctxt, t, net, net.Alice, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - - chanPoints = append(chanPoints, chanPoint) - } - - // Now that the channels are open, we should be able to fetch the - // backups of each of the channels. - for _, chanPoint := range chanPoints { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - req := &lnrpc.ExportChannelBackupRequest{ - ChanPoint: chanPoint, - } - chanBackup, err := carol.ExportChannelBackup(ctxt, req) - if err != nil { - t.Fatalf("unable to fetch backup for channel %v: %v", - chanPoint, err) - } - - // The returned backup should be full populated. Since it's - // encrypted, we can't assert any more than that atm. - if len(chanBackup.ChanBackup) == 0 { - t.Fatalf("obtained empty backup for channel: %v", chanPoint) - } - - // The specified chanPoint in the response should match our - // requested chanPoint. - if chanBackup.ChanPoint.String() != chanPoint.String() { - t.Fatalf("chanPoint mismatched: expected %v, got %v", - chanPoint.String(), - chanBackup.ChanPoint.String()) - } - } - - // Before we proceed, we'll make two utility methods we'll use below - // for our primary assertions. - assertNumSingleBackups := func(numSingles int) { - err := wait.NoError(func() error { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - req := &lnrpc.ChanBackupExportRequest{} - chanSnapshot, err := carol.ExportAllChannelBackups( - ctxt, req, - ) - if err != nil { - return fmt.Errorf("unable to export channel "+ - "backup: %v", err) - } - - if chanSnapshot.SingleChanBackups == nil { - return fmt.Errorf("single chan backups not " + - "populated") - } - - backups := chanSnapshot.SingleChanBackups.ChanBackups - if len(backups) != numSingles { - return fmt.Errorf("expected %v singles, "+ - "got %v", len(backups), numSingles) - } - - return nil - }, defaultTimeout) - if err != nil { - t.Fatalf(err.Error()) - } - } - assertMultiBackupFound := func() func(bool, map[wire.OutPoint]struct{}) { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - req := &lnrpc.ChanBackupExportRequest{} - chanSnapshot, err := carol.ExportAllChannelBackups(ctxt, req) - if err != nil { - t.Fatalf("unable to export channel backup: %v", err) - } - - return func(found bool, chanPoints map[wire.OutPoint]struct{}) { - switch { - case found && chanSnapshot.MultiChanBackup == nil: - t.Fatalf("multi-backup not present") - - case !found && chanSnapshot.MultiChanBackup != nil && - (len(chanSnapshot.MultiChanBackup.MultiChanBackup) != - chanbackup.NilMultiSizePacked): - - t.Fatalf("found multi-backup when non should " + - "be found") - } - - if !found { - return - } - - backedUpChans := chanSnapshot.MultiChanBackup.ChanPoints - if len(chanPoints) != len(backedUpChans) { - t.Fatalf("expected %v chans got %v", len(chanPoints), - len(backedUpChans)) - } - - for _, chanPoint := range backedUpChans { - wirePoint := rpcPointToWirePoint(t, chanPoint) - if _, ok := chanPoints[wirePoint]; !ok { - t.Fatalf("unexpected backup: %v", wirePoint) - } - } - } - } - - chans := make(map[wire.OutPoint]struct{}) - for _, chanPoint := range chanPoints { - chans[rpcPointToWirePoint(t, chanPoint)] = struct{}{} - } - - // We should have exactly two single channel backups contained, and we - // should also have a multi-channel backup. - assertNumSingleBackups(2) - assertMultiBackupFound()(true, chans) - - // We'll now close each channel on by one. After we close a channel, we - // shouldn't be able to find that channel as a backup still. We should - // also have one less single written to disk. - for i, chanPoint := range chanPoints { - ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) - closeChannelAndAssert( - ctxt, t, net, net.Alice, chanPoint, false, - ) - - assertNumSingleBackups(len(chanPoints) - i - 1) - - delete(chans, rpcPointToWirePoint(t, chanPoint)) - assertMultiBackupFound()(true, chans) - } - - // At this point we shouldn't have any single or multi-chan backups at - // all. - assertNumSingleBackups(0) - assertMultiBackupFound()(false, nil) -} - -// nodeRestorer is a function closure that allows each chanRestoreTestCase to -// control exactly *how* the prior node is restored. This might be using an -// backup obtained over RPC, or the file system, etc. -type nodeRestorer func() (*lntest.HarnessNode, error) - -// chanRestoreTestCase describes a test case for an end to end SCB restoration -// work flow. One node will start from scratch using an existing SCB. At the -// end of the est, both nodes should be made whole via the DLP protocol. -type chanRestoreTestCase struct { - // name is the name of the target test case. - name string - - // channelsUpdated is false then this means that no updates - // have taken place within the channel before restore. - // Otherwise, HTLCs will be settled between the two parties - // before restoration modifying the balance beyond the initial - // allocation. - channelsUpdated bool - - // initiator signals if Dave should be the one that opens the - // channel to Alice, or if it should be the other way around. - initiator bool - - // private signals if the channel from Dave to Carol should be - // private or not. - private bool - - // unconfirmed signals if the channel from Dave to Carol should be - // confirmed or not. - unconfirmed bool - - // anchorCommit is true, then the new anchor commitment type will be - // used for the channels created in the test. - anchorCommit bool - - // restoreMethod takes an old node, then returns a function - // closure that'll return the same node, but with its state - // restored via a custom method. We use this to abstract away - // _how_ a node is restored from our assertions once the node - // has been fully restored itself. - restoreMethod func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) -} - -// testChanRestoreScenario executes a chanRestoreTestCase from end to end, -// ensuring that after Dave restores his channel state according to the -// testCase, the DLP protocol is executed properly and both nodes are made -// whole. -func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, - testCase *chanRestoreTestCase, password []byte) { - - const ( - chanAmt = btcutil.Amount(10000000) - pushAmt = btcutil.Amount(5000000) - ) - - ctxb := context.Background() - - var nodeArgs []string - if testCase.anchorCommit { - nodeArgs = commitTypeAnchors.Args() - } - - // First, we'll create a brand new node we'll use within the test. If - // we have a custom backup file specified, then we'll also create that - // for use. - dave, mnemonic, err := net.NewNodeWithSeed( - "dave", nodeArgs, password, - ) - if err != nil { - t.Fatalf("unable to create new node: %v", err) - } - // Defer to a closure instead of to shutdownAndAssert due to the value - // of 'dave' changing throughout the test. - defer func() { - shutdownAndAssert(net, t, dave) - }() - carol, err := net.NewNode("carol", nodeArgs) - if err != nil { - t.Fatalf("unable to make new node: %v", err) - } - defer shutdownAndAssert(net, t, carol) - - // Now that our new nodes are created, we'll give them some coins for - // channel opening and anchor sweeping. - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol) - if err != nil { - t.Fatalf("unable to send coins to dave: %v", err) - } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, dave) - if err != nil { - t.Fatalf("unable to send coins to dave: %v", err) - } - - var from, to *lntest.HarnessNode - if testCase.initiator { - from, to = dave, carol - } else { - from, to = carol, dave - } - - // Next, we'll connect Dave to Carol, and open a new channel to her - // with a portion pushed. - if err := net.ConnectNodes(ctxt, dave, carol); err != nil { - t.Fatalf("unable to connect dave to carol: %v", err) - } - - // We will either open a confirmed or unconfirmed channel, depending on - // the requirements of the test case. - switch { - case testCase.unconfirmed: - ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) - _, err := net.OpenPendingChannel( - ctxt, from, to, chanAmt, pushAmt, - ) - if err != nil { - t.Fatalf("couldn't open pending channel: %v", err) - } - - // Give the pubsub some time to update the channel backup. - err = wait.NoError(func() error { - fi, err := os.Stat(dave.ChanBackupPath()) - if err != nil { - return err - } - if fi.Size() <= chanbackup.NilMultiSizePacked { - return fmt.Errorf("backup file empty") - } - return nil - }, defaultTimeout) - if err != nil { - t.Fatalf("channel backup not updated in time: %v", err) - } - - default: - ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) - chanPoint := openChannelAndAssert( - ctxt, t, net, from, to, - lntest.OpenChannelParams{ - Amt: chanAmt, - PushAmt: pushAmt, - Private: testCase.private, - }, - ) - - // Wait for both sides to see the opened channel. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - err = dave.WaitForNetworkChannelOpen(ctxt, chanPoint) - if err != nil { - t.Fatalf("dave didn't report channel: %v", err) - } - err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint) - if err != nil { - t.Fatalf("carol didn't report channel: %v", err) - } - } - - // If both parties should start with existing channel updates, then - // we'll send+settle an HTLC between 'from' and 'to' now. - if testCase.channelsUpdated { - invoice := &lnrpc.Invoice{ - Memo: "testing", - Value: 10000, - } - invoiceResp, err := to.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - err = completePaymentRequests( - ctxt, from, from.RouterClient, - []string{invoiceResp.PaymentRequest}, true, - ) - if err != nil { - t.Fatalf("unable to complete payments: %v", err) - } - } - - // Before we start the recovery, we'll record the balances of both - // Carol and Dave to ensure they both sweep their coins at the end. - 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 - - daveBalance, err := dave.WalletBalance(ctxt, balReq) - if err != nil { - t.Fatalf("unable to get carol's balance: %v", err) - } - daveStartingBalance := daveBalance.ConfirmedBalance - - // At this point, we'll now execute the restore method to give us the - // new node we should attempt our assertions against. - backupFilePath := dave.ChanBackupPath() - restoredNodeFunc, err := testCase.restoreMethod( - dave, backupFilePath, mnemonic, - ) - if err != nil { - t.Fatalf("unable to prep node restoration: %v", err) - } - - // TODO(roasbeef): assert recovery state in channel - - // Now that we're able to make our restored now, we'll shutdown the old - // Dave node as we'll be storing it shortly below. - shutdownAndAssert(net, t, dave) - - // Next, we'll make a new Dave and start the bulk of our recovery - // workflow. - dave, err = restoredNodeFunc() - if err != nil { - t.Fatalf("unable to restore node: %v", err) - } - - // First ensure that the on-chain balance is restored. - err = wait.NoError(func() error { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - balReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, balReq) - if err != nil { - return err - } - - daveBal := daveBalResp.ConfirmedBalance - if daveBal <= 0 { - return fmt.Errorf("expected positive balance, had %v", - daveBal) - } - - return nil - }, defaultTimeout) - if err != nil { - t.Fatalf("On-chain balance not restored: %v", err) - } - - // Now that we have our new node up, we expect that it'll re-connect to - // Carol automatically based on the restored backup. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - err = net.EnsureConnected(ctxt, dave, carol) - if err != nil { - t.Fatalf("node didn't connect after recovery: %v", err) - } - - // TODO(roasbeef): move dave restarts? - - // Now we'll assert that both sides properly execute the DLP protocol. - // We grab their balances now to ensure that they're made whole at the - // end of the protocol. - assertDLPExecuted( - net, t, carol, carolStartingBalance, dave, daveStartingBalance, - testCase.anchorCommit, - ) -} - -// chanRestoreViaRPC is a helper test method that returns a nodeRestorer -// instance which will restore the target node from a password+seed, then -// trigger a SCB restore using the RPC interface. -func chanRestoreViaRPC(net *lntest.NetworkHarness, - password []byte, mnemonic []string, - multi []byte) (nodeRestorer, error) { - - backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ - MultiChanBackup: multi, - } - - ctxb := context.Background() - - return func() (*lntest.HarnessNode, error) { - newNode, err := net.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, 1000, nil, - ) - if err != nil { - return nil, fmt.Errorf("unable to "+ - "restore node: %v", err) - } - - _, err = newNode.RestoreChannelBackups( - ctxb, &lnrpc.RestoreChanBackupRequest{ - Backup: backup, - }, - ) - if err != nil { - return nil, fmt.Errorf("unable "+ - "to restore backups: %v", err) - } - - return newNode, nil - }, nil -} - -// testChannelBackupRestore tests that we're able to recover from, and initiate -// the DLP protocol via: the RPC restore command, restoring on unlock, and -// restoring from initial wallet creation. We'll also alternate between -// restoring form the on disk file, and restoring from the exported RPC command -// as well. -func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { - password := []byte("El Psy Kongroo") - - ctxb := context.Background() - - var testCases = []chanRestoreTestCase{ - // Restore from backups obtained via the RPC interface. Dave - // was the initiator, of the non-advertised channel. - { - name: "restore from RPC backup", - channelsUpdated: false, - initiator: true, - private: false, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // For this restoration method, we'll grab the - // current multi-channel backup from the old - // node, and use it to restore a new node - // within the closure. - req := &lnrpc.ChanBackupExportRequest{} - chanBackup, err := oldNode.ExportAllChannelBackups( - ctxb, req, - ) - if err != nil { - return nil, fmt.Errorf("unable to obtain "+ - "channel backup: %v", err) - } - - multi := chanBackup.MultiChanBackup.MultiChanBackup - - // In our nodeRestorer function, we'll restore - // the node from seed, then manually recover - // the channel backup. - return chanRestoreViaRPC( - net, password, mnemonic, multi, - ) - }, - }, - - // Restore the backup from the on-disk file, using the RPC - // interface. - { - name: "restore from backup file", - initiator: true, - private: false, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // Read the entire Multi backup stored within - // this node's channels.backup file. - multi, err := ioutil.ReadFile(backupFilePath) - if err != nil { - return nil, err - } - - // Now that we have Dave's backup file, we'll - // create a new nodeRestorer that will restore - // using the on-disk channels.backup. - return chanRestoreViaRPC( - net, password, mnemonic, multi, - ) - }, - }, - - // Restore the backup as part of node initialization with the - // prior mnemonic and new backup seed. - { - name: "restore during creation", - initiator: true, - private: false, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // First, fetch the current backup state as is, - // to obtain our latest Multi. - chanBackup, err := oldNode.ExportAllChannelBackups( - ctxb, &lnrpc.ChanBackupExportRequest{}, - ) - if err != nil { - return nil, fmt.Errorf("unable to obtain "+ - "channel backup: %v", err) - } - backupSnapshot := &lnrpc.ChanBackupSnapshot{ - MultiChanBackup: chanBackup.MultiChanBackup, - } - - // Create a new nodeRestorer that will restore - // the node using the Multi backup we just - // obtained above. - return func() (*lntest.HarnessNode, error) { - return net.RestoreNodeWithSeed( - "dave", nil, password, - mnemonic, 1000, backupSnapshot, - ) - }, nil - }, - }, - - // Restore the backup once the node has already been - // re-created, using the Unlock call. - { - name: "restore during unlock", - initiator: true, - private: false, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // First, fetch the current backup state as is, - // to obtain our latest Multi. - chanBackup, err := oldNode.ExportAllChannelBackups( - ctxb, &lnrpc.ChanBackupExportRequest{}, - ) - if err != nil { - return nil, fmt.Errorf("unable to obtain "+ - "channel backup: %v", err) - } - backupSnapshot := &lnrpc.ChanBackupSnapshot{ - MultiChanBackup: chanBackup.MultiChanBackup, - } - - // Create a new nodeRestorer that will restore - // the node with its seed, but no channel - // backup, shutdown this initialized node, then - // restart it again using Unlock. - return func() (*lntest.HarnessNode, error) { - newNode, err := net.RestoreNodeWithSeed( - "dave", nil, password, - mnemonic, 1000, nil, - ) - if err != nil { - return nil, err - } - - err = net.RestartNode( - newNode, nil, backupSnapshot, - ) - if err != nil { - return nil, err - } - - return newNode, nil - }, nil - }, - }, - - // Restore the backup from the on-disk file a second time to - // make sure imports can be canceled and later resumed. - { - name: "restore from backup file twice", - initiator: true, - private: false, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // Read the entire Multi backup stored within - // this node's channels.backup file. - multi, err := ioutil.ReadFile(backupFilePath) - if err != nil { - return nil, err - } - - // Now that we have Dave's backup file, we'll - // create a new nodeRestorer that will restore - // using the on-disk channels.backup. - backup := &lnrpc.RestoreChanBackupRequest_MultiChanBackup{ - MultiChanBackup: multi, - } - - ctxb := context.Background() - - return func() (*lntest.HarnessNode, error) { - newNode, err := net.RestoreNodeWithSeed( - "dave", nil, password, mnemonic, - 1000, nil, - ) - if err != nil { - return nil, fmt.Errorf("unable to "+ - "restore node: %v", err) - } - - _, err = newNode.RestoreChannelBackups( - ctxb, - &lnrpc.RestoreChanBackupRequest{ - Backup: backup, - }, - ) - if err != nil { - return nil, fmt.Errorf("unable "+ - "to restore backups: %v", - err) - } - - _, err = newNode.RestoreChannelBackups( - ctxb, - &lnrpc.RestoreChanBackupRequest{ - Backup: backup, - }, - ) - if err != nil { - return nil, fmt.Errorf("unable "+ - "to restore backups the"+ - "second time: %v", - err) - } - - return newNode, nil - }, nil - }, - }, - - // Use the channel backup file that contains an unconfirmed - // channel and make sure recovery works as well. - { - name: "restore unconfirmed channel file", - channelsUpdated: false, - initiator: true, - private: false, - unconfirmed: true, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // Read the entire Multi backup stored within - // this node's channels.backup file. - multi, err := ioutil.ReadFile(backupFilePath) - if err != nil { - return nil, err - } - - // Let's assume time passes, the channel - // confirms in the meantime but for some reason - // the backup we made while it was still - // unconfirmed is the only backup we have. We - // should still be able to restore it. To - // simulate time passing, we mine some blocks - // to get the channel confirmed _after_ we saved - // the backup. - mineBlocks(t, net, 6, 1) - - // In our nodeRestorer function, we'll restore - // the node from seed, then manually recover - // the channel backup. - return chanRestoreViaRPC( - net, password, mnemonic, multi, - ) - }, - }, - - // Create a backup using RPC that contains an unconfirmed - // channel and make sure recovery works as well. - { - name: "restore unconfirmed channel RPC", - channelsUpdated: false, - initiator: true, - private: false, - unconfirmed: true, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // For this restoration method, we'll grab the - // current multi-channel backup from the old - // node. The channel should be included, even if - // it is not confirmed yet. - req := &lnrpc.ChanBackupExportRequest{} - chanBackup, err := oldNode.ExportAllChannelBackups( - ctxb, req, - ) - if err != nil { - return nil, fmt.Errorf("unable to obtain "+ - "channel backup: %v", err) - } - chanPoints := chanBackup.MultiChanBackup.ChanPoints - if len(chanPoints) == 0 { - return nil, fmt.Errorf("unconfirmed " + - "channel not included in backup") - } - - // Let's assume time passes, the channel - // confirms in the meantime but for some reason - // the backup we made while it was still - // unconfirmed is the only backup we have. We - // should still be able to restore it. To - // simulate time passing, we mine some blocks - // to get the channel confirmed _after_ we saved - // the backup. - mineBlocks(t, net, 6, 1) - - // In our nodeRestorer function, we'll restore - // the node from seed, then manually recover - // the channel backup. - multi := chanBackup.MultiChanBackup.MultiChanBackup - return chanRestoreViaRPC( - net, password, mnemonic, multi, - ) - }, - }, - - // Restore the backup from the on-disk file, using the RPC - // interface, for anchor commitment channels. - { - name: "restore from backup file anchors", - initiator: true, - private: false, - anchorCommit: true, - restoreMethod: func(oldNode *lntest.HarnessNode, - backupFilePath string, - mnemonic []string) (nodeRestorer, error) { - - // Read the entire Multi backup stored within - // this node's channels.backup file. - multi, err := ioutil.ReadFile(backupFilePath) - if err != nil { - return nil, err - } - - // Now that we have Dave's backup file, we'll - // create a new nodeRestorer that will restore - // using the on-disk channels.backup. - return chanRestoreViaRPC( - net, password, mnemonic, multi, - ) - }, - }, - } - - // TODO(roasbeef): online vs offline close? - - // TODO(roasbeef): need to re-trigger the on-disk file once the node - // ann is updated? - - for _, testCase := range testCases { - success := t.t.Run(testCase.name, func(t *testing.T) { - h := newHarnessTest(t, net) - testChanRestoreScenario(h, net, &testCase, password) - }) - if !success { - break - } - } -} - // testHoldInvoicePersistence tests that a sender to a hold-invoice, can be // restarted before the payment gets settled, and still be able to receive the // preimage.