itest: move channel backup tests to own file
This commit is contained in:
parent
fe834bc0c0
commit
0655a1bcfb
1022
lntest/itest/lnd_channel_backup_test.go
Normal file
1022
lntest/itest/lnd_channel_backup_test.go
Normal file
@ -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
|
||||
}
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user