chanbackup: introduce Multi, a multi-channel backup

In this commit, we introduce the Multi sturct. Multi is a series of
static channel backups. This type of backup can contains ALL the channel
backup state in a single packed blob. This is suitable for storing on
your file system, cloud storage, etc. Systems will be in place within
lnd to ensure that one can easily obtain the latest version of the Multi
for the node, and also that it will be kept up to date if channel state
changes.
This commit is contained in:
Olaoluwa Osuntokun 2018-12-09 18:49:01 -08:00
parent ee71e16593
commit 71df4b0545
No known key found for this signature in database
GPG Key ID: CE58F7F8E20FD9A2
2 changed files with 335 additions and 0 deletions

176
chanbackup/multi.go Normal file

@ -0,0 +1,176 @@
package chanbackup
import (
"bytes"
"fmt"
"io"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
)
// MultiBackupVersion denotes the version of the multi channel static channel
// backup. Based on this version, we know how to encode/decode packed/unpacked
// versions of multi backups.
type MultiBackupVersion byte
const (
// DefaultMultiVersion is the default version of the multi channel
// backup. The serialized format for this version is simply: version ||
// numBackups || SCBs...
DefaultMultiVersion = 0
)
// Multi is a form of static channel backup that is amenable to being
// serialized in a single file. Rather than a series of ciphertexts, a
// multi-chan backup is a single ciphertext of all static channel backups
// concatenated. This form factor gives users a single blob that they can use
// to safely copy/obtain at anytime to backup their channels.
type Multi struct {
// Version is the version that should be observed when attempting to
// pack the multi backup.
Version MultiBackupVersion
// StaticBackups is the set of single channel backups that this multi
// backup is comprised of.
StaticBackups []Single
}
// PackToWriter packs (encrypts+serializes) the target set of static channel
// backups into a single AEAD ciphertext into the passed io.Writer. This is the
// opposite of UnpackFromReader. The plaintext form of a multi-chan backup is
// the following: a 4 byte integer denoting the number of serialized static
// channel backups serialized, a series of serialized static channel backups
// concatenated. To pack this payload, we then apply our chacha20 AEAD to the
// entire payload, using the 24-byte nonce as associated data.
func (m Multi) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error {
// The only version that we know how to pack atm is version 0. Attempts
// to pack any other version will result in an error.
switch m.Version {
case DefaultMultiVersion:
break
default:
return fmt.Errorf("unable to pack unknown multi-version "+
"of %v", m.Version)
}
var multiBackupBuffer bytes.Buffer
// First, we'll write out the version of this multi channel baackup.
err := lnwire.WriteElements(&multiBackupBuffer, byte(m.Version))
if err != nil {
return err
}
// Now that we've written out the version of this multi-pack format,
// we'll now write the total number of backups to expect after this
// point.
numBackups := uint32(len(m.StaticBackups))
err = lnwire.WriteElements(&multiBackupBuffer, numBackups)
if err != nil {
return err
}
// Next, we'll serialize the raw plaintext version of each of the
// backup into the intermediate buffer.
for _, chanBackup := range m.StaticBackups {
err := chanBackup.Serialize(&multiBackupBuffer)
if err != nil {
return fmt.Errorf("unable to serialize backup "+
"for %v: %v", chanBackup.FundingOutpoint, err)
}
}
// With the plaintext multi backup assembled, we'll now encrypt it
// directly to the passed writer.
return encryptPayloadToWriter(multiBackupBuffer, w, keyRing)
}
// UnpackFromReader attempts to unpack (decrypt+deserialize) a packed
// multi-chan backup form the passed io.Reader. If we're unable to decrypt the
// any portion of the multi-chan backup, an error will be returned.
func (m *Multi) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error {
// We'll attempt to read the entire packed backup, and also decrypt it
// using the passed key ring which is expected to be able to derive the
// encryption keys.
plaintextBackup, err := decryptPayloadFromReader(r, keyRing)
if err != nil {
return err
}
backupReader := bytes.NewReader(plaintextBackup)
// Now that we've decrypted the payload successfully, we can parse out
// each of the individual static channel backups.
// First, we'll need to read the version of this multi-back up so we
// can know how to unpack each of the individual SCB's.
var multiVersion byte
err = lnwire.ReadElements(backupReader, &multiVersion)
if err != nil {
return err
}
m.Version = MultiBackupVersion(multiVersion)
switch m.Version {
// The default version is simply a set of serialized SCB's with the
// number of total SCB's prepended to the front of the byte slice.
case DefaultMultiVersion:
// First, we'll need to read out the total number of backups
// that've been serialized into this multi-chan backup. Each
// backup is the same size, so we can continue until we've
// parsed out everything.
var numBackups uint32
err = lnwire.ReadElements(backupReader, &numBackups)
if err != nil {
return err
}
// We'll continue to parse out each backup until we've read all
// that was indicated from the length prefix.
for ; numBackups != 0; numBackups-- {
// Attempt to parse out the net static channel backup,
// if it's been malformed, then we'll return with an
// error
var chanBackup Single
err := chanBackup.Deserialize(backupReader)
if err != nil {
return err
}
// Collect the next valid chan backup into the main
// multi backup slice.
m.StaticBackups = append(m.StaticBackups, chanBackup)
}
default:
return fmt.Errorf("unable to unpack unknown multi-version "+
"of %v", multiVersion)
}
return nil
}
// TODO(roasbeef): new key ring interface?
// * just returns key given params?
// PackedMulti represents a raw fully packed (serialized+encrypted)
// multi-channel static channel backup.
type PackedMulti []byte
// Unpack attempts to unpack (decrypt+desrialize) the target packed
// multi-channel back up. If we're unable to fully unpack this back, then an
// error will be returned.
func (p *PackedMulti) Unpack(keyRing keychain.KeyRing) (*Multi, error) {
var m Multi
packedReader := bytes.NewReader(*p)
if err := m.UnpackFromReader(packedReader, keyRing); err != nil {
return nil, err
}
return &m, nil
}
// TODO(roasbsef): fuzz parsing

159
chanbackup/multi_test.go Normal file

@ -0,0 +1,159 @@
package chanbackup
import (
"bytes"
"net"
"testing"
)
// TestMultiPackUnpack...
func TestMultiPackUnpack(t *testing.T) {
t.Parallel()
var multi Multi
numSingles := 10
originalSingles := make([]Single, 0, numSingles)
for i := 0; i < numSingles; i++ {
channel, err := genRandomOpenChannelShell()
if err != nil {
t.Fatalf("unable to gen channel: %v", err)
}
single := NewSingle(channel, []net.Addr{addr1, addr2})
originalSingles = append(originalSingles, single)
multi.StaticBackups = append(multi.StaticBackups, single)
}
keyRing := &mockKeyRing{}
versionTestCases := []struct {
// version is the pack/unpack version that we should use to
// decode/encode the final SCB.
version MultiBackupVersion
// valid tests us if this test case should pass or not.
valid bool
}{
// The default version, should pack/unpack with no problem.
{
version: DefaultSingleVersion,
valid: true,
},
// A non-default version, atm this should result in a failure.
{
version: 99,
valid: false,
},
}
for i, versionCase := range versionTestCases {
multi.Version = versionCase.version
var b bytes.Buffer
err := multi.PackToWriter(&b, keyRing)
switch {
// If this is a valid test case, and we failed, then we'll
// return an error.
case err != nil && versionCase.valid:
t.Fatalf("#%v, unable to pack multi: %v", i, err)
// If this is an invalid test case, and we passed it, then
// we'll return an error.
case err == nil && !versionCase.valid:
t.Fatalf("#%v got nil error for invalid pack: %v",
i, err)
}
// If this is a valid test case, then we'll continue to ensure
// we can unpack it, and also that if we mutate the packed
// version, then we trigger an error.
if versionCase.valid {
var unpackedMulti Multi
err = unpackedMulti.UnpackFromReader(&b, keyRing)
if err != nil {
t.Fatalf("#%v unable to unpack multi: %v",
i, err)
}
// First, we'll ensure that the unpacked version of the
// packed multi is the same as the original set.
if len(originalSingles) !=
len(unpackedMulti.StaticBackups) {
t.Fatalf("expected %v singles, got %v",
len(originalSingles),
len(unpackedMulti.StaticBackups))
}
for i := 0; i < numSingles; i++ {
assertSingleEqual(
t, originalSingles[i],
unpackedMulti.StaticBackups[i],
)
}
// Next, we'll make a fake packed multi, it'll have an
// unknown version relative to what's implemented atm.
var fakePackedMulti bytes.Buffer
fakeRawMulti := bytes.NewBuffer(
bytes.Repeat([]byte{99}, 20),
)
err := encryptPayloadToWriter(
*fakeRawMulti, &fakePackedMulti, keyRing,
)
if err != nil {
t.Fatalf("unable to pack fake multi; %v", err)
}
// We should reject this fake multi as it contains an
// unknown version.
err = unpackedMulti.UnpackFromReader(
&fakePackedMulti, keyRing,
)
if err == nil {
t.Fatalf("#%v unpack with unknown version "+
"should have failed", i)
}
}
}
}
// TestPackedMultiUnpack tests that we're able to properly unpack a typed
// packed multi.
func TestPackedMultiUnpack(t *testing.T) {
t.Parallel()
keyRing := &mockKeyRing{}
// First, we'll make a new unpacked multi with a random channel.
testChannel, err := genRandomOpenChannelShell()
if err != nil {
t.Fatalf("unable to gen random channel: %v", err)
}
var multi Multi
multi.StaticBackups = append(
multi.StaticBackups, NewSingle(testChannel, nil),
)
// Now that we have our multi, we'll pack it into a new buffer.
var b bytes.Buffer
if err := multi.PackToWriter(&b, keyRing); err != nil {
t.Fatalf("unable to pack multi: %v", err)
}
// We should be able to properly unpack this typed packed multi.
packedMulti := PackedMulti(b.Bytes())
unpackedMulti, err := packedMulti.Unpack(keyRing)
if err != nil {
t.Fatalf("unable to unpack multi: %v", err)
}
// Finally, the versions should match, and the unpacked singles also
// identical.
if multi.Version != unpackedMulti.Version {
t.Fatalf("version mismatch: expected %v got %v",
multi.Version, unpackedMulti.Version)
}
assertSingleEqual(
t, multi.StaticBackups[0], unpackedMulti.StaticBackups[0],
)
}