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 +}