diff --git a/chanbackup/multi.go b/chanbackup/multi.go new file mode 100644 index 00000000..d77be204 --- /dev/null +++ b/chanbackup/multi.go @@ -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 diff --git a/chanbackup/multi_test.go b/chanbackup/multi_test.go new file mode 100644 index 00000000..a6317e09 --- /dev/null +++ b/chanbackup/multi_test.go @@ -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], + ) +}