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:
parent
ee71e16593
commit
71df4b0545
176
chanbackup/multi.go
Normal file
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
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],
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user