diff --git a/chanbackup/backup.go b/chanbackup/backup.go new file mode 100644 index 00000000..ca3698a5 --- /dev/null +++ b/chanbackup/backup.go @@ -0,0 +1,99 @@ +package chanbackup + +import ( + "fmt" + "net" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" +) + +// LiveChannelSource is an interface that allows us to query for the set of +// live channels. A live channel is one that is open, and has not had a +// commitment transaction broadcast. +type LiveChannelSource interface { + // FetchAllChannels returns all known live channels. + FetchAllChannels() ([]*channeldb.OpenChannel, error) + + // FetchChannel attempts to locate a live channel identified by the + // passed chanPoint. + FetchChannel(chanPoint wire.OutPoint) (*channeldb.OpenChannel, error) + + // AddrsForNode returns all known addresses for the target node public + // key. + AddrsForNode(nodePub *btcec.PublicKey) ([]net.Addr, error) +} + +// assembleChanBackup attempts to assemble a static channel backup for the +// passed open channel. The backup includes all information required to restore +// the channel, as well as addressing information so we can find the peer and +// reconnect to them to initiate the protocol. +func assembleChanBackup(chanSource LiveChannelSource, + openChan *channeldb.OpenChannel) (*Single, error) { + + log.Debugf("Crafting backup for ChannelPoint(%v)", + openChan.FundingOutpoint) + + // First, we'll query the channel source to obtain all the addresses + // that are are associated with the peer for this channel. + nodeAddrs, err := chanSource.AddrsForNode(openChan.IdentityPub) + if err != nil { + return nil, err + } + + single := NewSingle(openChan, nodeAddrs) + + return &single, nil +} + +// FetchBackupForChan attempts to create a plaintext static channel backup for +// the target channel identified by its channel point. If we're unable to find +// the target channel, then an error will be returned. +func FetchBackupForChan(chanPoint wire.OutPoint, + chanSource LiveChannelSource) (*Single, error) { + + // First, we'll query the channel source to see if the channel is known + // and open within the database. + targetChan, err := chanSource.FetchChannel(chanPoint) + if err != nil { + // If we can't find the channel, then we return with an error, + // as we have nothing to backup. + return nil, fmt.Errorf("unable to find target channel") + } + + // Once we have the target channel, we can assemble the backup using + // the source to obtain any extra information that we may need. + staticChanBackup, err := assembleChanBackup(chanSource, targetChan) + if err != nil { + return nil, fmt.Errorf("unable to create chan backup: %v", err) + } + + return staticChanBackup, nil +} + +// FetchStaticChanBackups will return a plaintext static channel back up for +// all known active/open channels within the passed channel source. +func FetchStaticChanBackups(chanSource LiveChannelSource) ([]Single, error) { + // First, we'll query the backup source for information concerning all + // currently open and available channels. + openChans, err := chanSource.FetchAllChannels() + if err != nil { + return nil, err + } + + // Now that we have all the channels, we'll use the chanSource to + // obtain any auxiliary information we need to craft a backup for each + // channel. + staticChanBackups := make([]Single, len(openChans)) + for i, openChan := range openChans { + chanBackup, err := assembleChanBackup(chanSource, openChan) + if err != nil { + return nil, err + } + + staticChanBackups[i] = *chanBackup + } + + return staticChanBackups, nil +} diff --git a/chanbackup/backup_test.go b/chanbackup/backup_test.go new file mode 100644 index 00000000..ea8bfcea --- /dev/null +++ b/chanbackup/backup_test.go @@ -0,0 +1,197 @@ +package chanbackup + +import ( + "fmt" + "net" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" +) + +type mockChannelSource struct { + chans map[wire.OutPoint]*channeldb.OpenChannel + + failQuery bool + + addrs map[[33]byte][]net.Addr +} + +func newMockChannelSource() *mockChannelSource { + return &mockChannelSource{ + chans: make(map[wire.OutPoint]*channeldb.OpenChannel), + addrs: make(map[[33]byte][]net.Addr), + } +} + +func (m *mockChannelSource) FetchAllChannels() ([]*channeldb.OpenChannel, error) { + if m.failQuery { + return nil, fmt.Errorf("fail") + } + + chans := make([]*channeldb.OpenChannel, 0, len(m.chans)) + for _, channel := range m.chans { + chans = append(chans, channel) + } + + return chans, nil +} + +func (m *mockChannelSource) FetchChannel(chanPoint wire.OutPoint) (*channeldb.OpenChannel, error) { + if m.failQuery { + return nil, fmt.Errorf("fail") + } + + channel, ok := m.chans[chanPoint] + if !ok { + return nil, fmt.Errorf("can't find chan") + } + + return channel, nil +} + +func (m *mockChannelSource) addAddrsForNode(nodePub *btcec.PublicKey, addrs []net.Addr) { + var nodeKey [33]byte + copy(nodeKey[:], nodePub.SerializeCompressed()) + + m.addrs[nodeKey] = addrs +} + +func (m *mockChannelSource) AddrsForNode(nodePub *btcec.PublicKey) ([]net.Addr, error) { + if m.failQuery { + return nil, fmt.Errorf("fail") + } + + var nodeKey [33]byte + copy(nodeKey[:], nodePub.SerializeCompressed()) + + addrs, ok := m.addrs[nodeKey] + if !ok { + return nil, fmt.Errorf("can't find addr") + } + + return addrs, nil +} + +// TestFetchBackupForChan tests that we're able to construct a single channel +// backup for channels that are known, unknown, and also channels in which we +// can find addresses for and otherwise. +func TestFetchBackupForChan(t *testing.T) { + t.Parallel() + + // First, we'll make two channels, only one of them will have all the + // information we need to construct set of backups for them. + randomChan1, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable to generate chan: %v", err) + } + randomChan2, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable to generate chan: %v", err) + } + + chanSource := newMockChannelSource() + chanSource.chans[randomChan1.FundingOutpoint] = randomChan1 + chanSource.chans[randomChan2.FundingOutpoint] = randomChan2 + + chanSource.addAddrsForNode(randomChan1.IdentityPub, []net.Addr{addr1}) + + testCases := []struct { + chanPoint wire.OutPoint + + pass bool + }{ + // Able to find channel, and addresses, should pass. + { + chanPoint: randomChan1.FundingOutpoint, + pass: true, + }, + + // Able to find channel, not able to find addrs, should fail. + { + chanPoint: randomChan2.FundingOutpoint, + pass: false, + }, + + // Not able to find channel, should fail. + { + chanPoint: op, + pass: false, + }, + } + for i, testCase := range testCases { + _, err := FetchBackupForChan(testCase.chanPoint, chanSource) + switch { + // If this is a valid test case, and we failed, then we'll + // return an error. + case err != nil && testCase.pass: + t.Fatalf("#%v, unable to make chan backup: %v", i, err) + + // If this is an invalid test case, and we passed it, then + // we'll return an error. + case err == nil && !testCase.pass: + t.Fatalf("#%v got nil error for invalid req: %v", + i, err) + } + } +} + +// TestFetchStaticChanBackups tests that we're able to properly query the +// channel source for all channels and construct a Single for each channel. +func TestFetchStaticChanBackups(t *testing.T) { + t.Parallel() + + // First, we'll make the set of channels that we want to seed the + // channel source with. Both channels will be fully populated in the + // channel source. + const numChans = 2 + randomChan1, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable to generate chan: %v", err) + } + randomChan2, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable to generate chan: %v", err) + } + + chanSource := newMockChannelSource() + chanSource.chans[randomChan1.FundingOutpoint] = randomChan1 + chanSource.chans[randomChan2.FundingOutpoint] = randomChan2 + chanSource.addAddrsForNode(randomChan1.IdentityPub, []net.Addr{addr1}) + chanSource.addAddrsForNode(randomChan2.IdentityPub, []net.Addr{addr2}) + + // With the channel source populated, we'll now attempt to create a set + // of backups for all the channels. This should succeed, as all items + // are populated within the channel source. + backups, err := FetchStaticChanBackups(chanSource) + if err != nil { + t.Fatalf("unable to create chan back ups: %v", err) + } + + if len(backups) != numChans { + t.Fatalf("expected %v chans, instead got %v", numChans, + len(backups)) + } + + // We'll attempt to create a set up backups again, but this time the + // second channel will have missing information, which should cause the + // query to fail. + var n [33]byte + copy(n[:], randomChan2.IdentityPub.SerializeCompressed()) + delete(chanSource.addrs, n) + + _, err = FetchStaticChanBackups(chanSource) + if err == nil { + t.Fatalf("query with incomplete information should fail") + } + + // To wrap up, we'll ensure that if we're unable to query the channel + // source at all, then we'll fail as well. + chanSource = newMockChannelSource() + chanSource.failQuery = true + _, err = FetchStaticChanBackups(chanSource) + if err == nil { + t.Fatalf("query should fail") + } +}