From 7fcab83bb4cc19f0658f30486f96628d417089c9 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 9 Dec 2018 19:02:40 -0800 Subject: [PATCH] chanbackup: add functions to allow recovery of existing channel backups In this commit, we add a series of functions that will allow users to recover existing channel backups. We do this using two primary interfaces: the ChannelRestorer, and the PeerConnector. The first interfaces allows us to abstract away the details w.r.t exactly how a channel is restored. Instead, we simply expect that the channel backup will be inserted as a sort of "channel shell" which contains only the data required to initiate the data loss protection protocol. The second interface is how we instruct the Lightning node to connect out to the channel peer given its known addresses. --- chanbackup/recover.go | 114 ++++++++++++++++++ chanbackup/recover_test.go | 232 +++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 chanbackup/recover.go create mode 100644 chanbackup/recover_test.go diff --git a/chanbackup/recover.go b/chanbackup/recover.go new file mode 100644 index 00000000..8619c880 --- /dev/null +++ b/chanbackup/recover.go @@ -0,0 +1,114 @@ +package chanbackup + +import ( + "net" + + "github.com/btcsuite/btcd/btcec" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/keychain" +) + +// ChannelRestorer is an interface that allows the Recover method to map the +// set of single channel backups into a set of "channel shells" and store these +// persistently on disk. The channel shell should contain all the information +// needed to execute the data loss recovery protocol once the channel peer is +// connected to. +type ChannelRestorer interface { + // RestoreChansFromSingles attempts to map the set of single channel + // backups to channel shells that will be stored persistently. Once + // these shells have been stored on disk, we'll be able to connect to + // the channel peer an execute the data loss recovery protocol. + RestoreChansFromSingles(...Single) error +} + +// PeerConnector is an interface that allows the Recover method to connect to +// the target node given the set of possible addresses. +type PeerConnector interface { + // ConnectPeer attempts to connect to the target node at the set of + // available addresses. Once this method returns with a non-nil error, + // the connector should attempt to persistently connect to the target + // peer in the background as a persistent attempt. + ConnectPeer(node *btcec.PublicKey, addrs []net.Addr) error +} + +// Recover attempts to recover the static channel state from a set of static +// channel backups. If successfully, the database will be populated with a +// series of "shell" channels. These "shell" channels cannot be used to operate +// the channel as normal, but instead are meant to be used to enter the data +// loss recovery phase, and recover the settled funds within +// the channel. In addition a LinkNode will be created for each new peer as +// well, in order to expose the addressing information required to locate to +// and connect to each peer in order to initiate the recovery protocol. +func Recover(backups []Single, restorer ChannelRestorer, + peerConnector PeerConnector) error { + + for _, backup := range backups { + log.Infof("Restoring ChannelPoint(%v) to disk: ", + backup.FundingOutpoint) + + err := restorer.RestoreChansFromSingles(backup) + if err != nil { + return err + } + + log.Infof("Attempting to connect to node=%x (addrs=%v) to "+ + "restore ChannelPoint(%v)", + backup.RemoteNodePub.SerializeCompressed(), + newLogClosure(func() string { + return spew.Sdump(backup.Addresses) + }), backup.FundingOutpoint) + + err = peerConnector.ConnectPeer( + backup.RemoteNodePub, backup.Addresses, + ) + if err != nil { + return err + } + + // TODO(roasbeef): to handle case where node has changed addrs, + // need to subscribe to new updates for target node pub to + // attempt to connect to other addrs + // + // * just to to fresh w/ call to node addrs and de-dup? + } + + return nil +} + +// TODO(roasbeef): more specific keychain interface? + +// UnpackAndRecoverSingles is a one-shot method, that given a set of packed +// single channel backups, will restore the channel state to a channel shell, +// and also reach out to connect to any of the known node addresses for that +// channel. It is assumes that after this method exists, if a connection we +// able to be established, then then PeerConnector will continue to attempt to +// re-establish a persistent connection in the background. +func UnpackAndRecoverSingles(singles PackedSingles, + keyChain keychain.KeyRing, restorer ChannelRestorer, + peerConnector PeerConnector) error { + + chanBackups, err := singles.Unpack(keyChain) + if err != nil { + return err + } + + return Recover(chanBackups, restorer, peerConnector) +} + +// UnpackAndRecoverMulti is a one-shot method, that given a set of packed +// multi-channel backups, will restore the channel states to channel shells, +// and also reach out to connect to any of the known node addresses for that +// channel. It is assumes that after this method exists, if a connection we +// able to be established, then then PeerConnector will continue to attempt to +// re-establish a persistent connection in the background. +func UnpackAndRecoverMulti(packedMulti PackedMulti, + keyChain keychain.KeyRing, restorer ChannelRestorer, + peerConnector PeerConnector) error { + + chanBackups, err := packedMulti.Unpack(keyChain) + if err != nil { + return err + } + + return Recover(chanBackups.StaticBackups, restorer, peerConnector) +} diff --git a/chanbackup/recover_test.go b/chanbackup/recover_test.go new file mode 100644 index 00000000..e2b9d71e --- /dev/null +++ b/chanbackup/recover_test.go @@ -0,0 +1,232 @@ +package chanbackup + +import ( + "bytes" + "fmt" + "net" + "testing" + + "github.com/btcsuite/btcd/btcec" +) + +type mockChannelRestorer struct { + fail bool + + callCount int +} + +func (m *mockChannelRestorer) RestoreChansFromSingles(...Single) error { + if m.fail { + return fmt.Errorf("fail") + } + + m.callCount++ + + return nil +} + +type mockPeerConnector struct { + fail bool + + callCount int +} + +func (m *mockPeerConnector) ConnectPeer(node *btcec.PublicKey, + addrs []net.Addr) error { + + if m.fail { + return fmt.Errorf("fail") + } + + m.callCount++ + + return nil +} + +// TestUnpackAndRecoverSingles tests that we're able to properly unpack and +// recover a set of packed singles. +func TestUnpackAndRecoverSingles(t *testing.T) { + t.Parallel() + + keyRing := &mockKeyRing{} + + // First, we'll create a number of single chan backups that we'll + // shortly back to so we can begin our recovery attempt. + numSingles := 10 + backups := make([]Single, 0, numSingles) + var packedBackups PackedSingles + for i := 0; i < numSingles; i++ { + channel, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable make channel: %v", err) + } + + single := NewSingle(channel, nil) + + var b bytes.Buffer + if err := single.PackToWriter(&b, keyRing); err != nil { + t.Fatalf("unable to pack single: %v", err) + } + + backups = append(backups, single) + packedBackups = append(packedBackups, b.Bytes()) + } + + chanRestorer := mockChannelRestorer{} + peerConnector := mockPeerConnector{} + + // Now that we have our backups (packed and unpacked), we'll attempt to + // restore them all in a single batch. + + // If we make the channel restore fail, then the entire method should + // as well + chanRestorer.fail = true + err := UnpackAndRecoverSingles( + packedBackups, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("restoration should have failed") + } + + chanRestorer.fail = false + + // If we make the peer connector fail, then the entire method should as + // well + peerConnector.fail = true + err = UnpackAndRecoverSingles( + packedBackups, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("restoration should have failed") + } + + chanRestorer.callCount-- + peerConnector.fail = false + + // Next, we'll ensure that if all the interfaces function as expected, + // then the channels will properly be unpacked and restored. + err = UnpackAndRecoverSingles( + packedBackups, keyRing, &chanRestorer, &peerConnector, + ) + if err != nil { + t.Fatalf("unable to recover chans: %v", err) + } + + // Both the restorer, and connector should have been called 10 times, + // once for each backup. + if chanRestorer.callCount != numSingles { + t.Fatalf("expected %v calls, instead got %v", + numSingles, chanRestorer.callCount) + } + if peerConnector.callCount != numSingles { + t.Fatalf("expected %v calls, instead got %v", + numSingles, peerConnector.callCount) + } + + // If we modify the keyRing, then unpacking should fail. + keyRing.fail = true + err = UnpackAndRecoverSingles( + packedBackups, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("unpacking should have failed") + } + + // TODO(roasbeef): verify proper call args +} + +// TestUnpackAndRecoverMulti tests that we're able to properly unpack and +// recover a packed multi. +func TestUnpackAndRecoverMulti(t *testing.T) { + t.Parallel() + + keyRing := &mockKeyRing{} + + // First, we'll create a number of single chan backups that we'll + // shortly back to so we can begin our recovery attempt. + numSingles := 10 + backups := make([]Single, 0, numSingles) + for i := 0; i < numSingles; i++ { + channel, err := genRandomOpenChannelShell() + if err != nil { + t.Fatalf("unable make channel: %v", err) + } + + single := NewSingle(channel, nil) + + backups = append(backups, single) + } + + multi := Multi{ + StaticBackups: backups, + } + + var b bytes.Buffer + if err := multi.PackToWriter(&b, keyRing); err != nil { + t.Fatalf("unable to pack multi: %v", err) + } + + // Next, we'll pack the set of singles into a packed multi, and also + // create the set of interfaces we need to carry out the remainder of + // the test. + packedMulti := PackedMulti(b.Bytes()) + + chanRestorer := mockChannelRestorer{} + peerConnector := mockPeerConnector{} + + // If we make the channel restore fail, then the entire method should + // as well + chanRestorer.fail = true + err := UnpackAndRecoverMulti( + packedMulti, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("restoration should have failed") + } + + chanRestorer.fail = false + + // If we make the peer connector fail, then the entire method should as + // well + peerConnector.fail = true + err = UnpackAndRecoverMulti( + packedMulti, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("restoration should have failed") + } + + chanRestorer.callCount-- + peerConnector.fail = false + + // Next, we'll ensure that if all the interfaces function as expected, + // then the channels will properly be unpacked and restored. + err = UnpackAndRecoverMulti( + packedMulti, keyRing, &chanRestorer, &peerConnector, + ) + if err != nil { + t.Fatalf("unable to recover chans: %v", err) + } + + // Both the restorer, and connector should have been called 10 times, + // once for each backup. + if chanRestorer.callCount != numSingles { + t.Fatalf("expected %v calls, instead got %v", + numSingles, chanRestorer.callCount) + } + if peerConnector.callCount != numSingles { + t.Fatalf("expected %v calls, instead got %v", + numSingles, peerConnector.callCount) + } + + // If we modify the keyRing, then unpacking should fail. + keyRing.fail = true + err = UnpackAndRecoverMulti( + packedMulti, keyRing, &chanRestorer, &peerConnector, + ) + if err == nil { + t.Fatalf("unpacking should have failed") + } + + // TODO(roasbeef): verify proper call args +}