From 866623e84b24efe96664ad549723fa736227ac7b Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 20 Feb 2020 18:08:01 +0100 Subject: [PATCH] channeldb/migration13: migrate to mpp structure This commit migrates the payments in the database to a new structure that allows for multiple htlcs per payments. The migration introduces a new sub-bucket that contains a list of htlcs and moves the old single htlc into that. --- channeldb/db.go | 6 + channeldb/log.go | 2 + channeldb/migration13/log.go | 14 ++ channeldb/migration13/migration.go | 202 ++++++++++++++++++++++++ channeldb/migration13/migration_test.go | 123 +++++++++++++++ 5 files changed, 347 insertions(+) create mode 100644 channeldb/migration13/log.go create mode 100644 channeldb/migration13/migration.go create mode 100644 channeldb/migration13/migration_test.go diff --git a/channeldb/db.go b/channeldb/db.go index c1cf46ba..cd4cd756 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/bbolt" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb/migration12" + "github.com/lightningnetwork/lnd/channeldb/migration13" "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lnwire" @@ -124,6 +125,11 @@ var ( number: 12, migration: migration12.MigrateInvoiceTLV, }, + { + // Migrate to multi-path payments. + number: 13, + migration: migration13.MigrateMPP, + }, } // Big endian is the preferred byte order, due to cursor scans over diff --git a/channeldb/log.go b/channeldb/log.go index 5229edbf..7490c6bf 100644 --- a/channeldb/log.go +++ b/channeldb/log.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btclog" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb/migration12" + "github.com/lightningnetwork/lnd/channeldb/migration13" "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" ) @@ -29,4 +30,5 @@ func UseLogger(logger btclog.Logger) { log = logger migration_01_to_11.UseLogger(logger) migration12.UseLogger(logger) + migration13.UseLogger(logger) } diff --git a/channeldb/migration13/log.go b/channeldb/migration13/log.go new file mode 100644 index 00000000..33ec1812 --- /dev/null +++ b/channeldb/migration13/log.go @@ -0,0 +1,14 @@ +package migration13 + +import ( + "github.com/btcsuite/btclog" +) + +// log is a logger that is initialized as disabled. This means the package will +// not perform any logging by default until a logger is set. +var log = btclog.Disabled + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/channeldb/migration13/migration.go b/channeldb/migration13/migration.go new file mode 100644 index 00000000..56fced62 --- /dev/null +++ b/channeldb/migration13/migration.go @@ -0,0 +1,202 @@ +package migration13 + +import ( + "encoding/binary" + "fmt" + + "github.com/coreos/bbolt" +) + +var ( + paymentsRootBucket = []byte("payments-root-bucket") + + // paymentCreationInfoKey is a key used in the payment's sub-bucket to + // store the creation info of the payment. + paymentCreationInfoKey = []byte("payment-creation-info") + + // paymentFailInfoKey is a key used in the payment's sub-bucket to + // store information about the reason a payment failed. + paymentFailInfoKey = []byte("payment-fail-info") + + // paymentAttemptInfoKey is a key used in the payment's sub-bucket to + // store the info about the latest attempt that was done for the + // payment in question. + paymentAttemptInfoKey = []byte("payment-attempt-info") + + // paymentSettleInfoKey is a key used in the payment's sub-bucket to + // store the settle info of the payment. + paymentSettleInfoKey = []byte("payment-settle-info") + + // paymentHtlcsBucket is a bucket where we'll store the information + // about the HTLCs that were attempted for a payment. + paymentHtlcsBucket = []byte("payment-htlcs-bucket") + + // htlcAttemptInfoKey is a key used in a HTLC's sub-bucket to store the + // info about the attempt that was done for the HTLC in question. + htlcAttemptInfoKey = []byte("htlc-attempt-info") + + // htlcSettleInfoKey is a key used in a HTLC's sub-bucket to store the + // settle info, if any. + htlcSettleInfoKey = []byte("htlc-settle-info") + + // htlcFailInfoKey is a key used in a HTLC's sub-bucket to store + // failure information, if any. + htlcFailInfoKey = []byte("htlc-fail-info") + + byteOrder = binary.BigEndian +) + +// MigrateMPP migrates the payments to a new structure that accommodates for mpp +// payments. +func MigrateMPP(tx *bbolt.Tx) error { + log.Infof("Migrating payments to mpp structure") + + // Iterate over all payments and store their indexing keys. This is + // needed, because no modifications are allowed inside a Bucket.ForEach + // loop. + paymentsBucket := tx.Bucket(paymentsRootBucket) + if paymentsBucket == nil { + return nil + } + + var paymentKeys [][]byte + err := paymentsBucket.ForEach(func(k, v []byte) error { + paymentKeys = append(paymentKeys, k) + return nil + }) + if err != nil { + return err + } + + // With all keys retrieved, start the migration. + for _, k := range paymentKeys { + bucket := paymentsBucket.Bucket(k) + + // We only expect sub-buckets to be found in + // this top-level bucket. + if bucket == nil { + return fmt.Errorf("non bucket element in " + + "payments bucket") + } + + // Fetch old format creation info. + creationInfo := bucket.Get(paymentCreationInfoKey) + if creationInfo == nil { + return fmt.Errorf("creation info not found") + } + + // Make a copy because bbolt doesn't allow this value to be + // changed in-place. + newCreationInfo := make([]byte, len(creationInfo)) + copy(newCreationInfo, creationInfo) + + // Convert to nano seconds. + timeBytes := newCreationInfo[32+8 : 32+8+8] + time := byteOrder.Uint64(timeBytes) + timeNs := time * 1000000000 + byteOrder.PutUint64(timeBytes, timeNs) + + // Write back new format creation info. + err := bucket.Put(paymentCreationInfoKey, newCreationInfo) + if err != nil { + return err + } + + // No migration needed if there is no attempt stored. + attemptInfo := bucket.Get(paymentAttemptInfoKey) + if attemptInfo == nil { + continue + } + + // Delete attempt info on the payment level. + if err := bucket.Delete(paymentAttemptInfoKey); err != nil { + return err + } + + // Save attempt id for later use. + attemptID := attemptInfo[:8] + + // Discard attempt id. It will become a bucket key in the new + // structure. + attemptInfo = attemptInfo[8:] + + // Append unknown (zero) attempt time. + var zero [8]byte + attemptInfo = append(attemptInfo, zero[:]...) + + // Create bucket that contains all htlcs. + htlcsBucket, err := bucket.CreateBucket(paymentHtlcsBucket) + if err != nil { + return err + } + + // Create an htlc for this attempt. + htlcBucket, err := htlcsBucket.CreateBucket(attemptID) + if err != nil { + return err + } + + // Save migrated attempt info. + err = htlcBucket.Put(htlcAttemptInfoKey, attemptInfo) + if err != nil { + return err + } + + // Migrate settle info. + settleInfo := bucket.Get(paymentSettleInfoKey) + if settleInfo != nil { + // Payment-level settle info can be deleted. + err := bucket.Delete(paymentSettleInfoKey) + if err != nil { + return err + } + + // Append unknown (zero) settle time. + settleInfo = append(settleInfo, zero[:]...) + + // Save settle info. + err = htlcBucket.Put(htlcSettleInfoKey, settleInfo) + if err != nil { + return err + } + + // Migration for settled htlc completed. + continue + } + + // If there is no payment-level failure reason, the payment is + // still in flight and nothing else needs to be migrated. + // Otherwise the payment-level failure reason can remain + // unchanged. + inFlight := bucket.Get(paymentFailInfoKey) == nil + if inFlight { + continue + } + + // The htlc failed. Add htlc fail info with reason unknown. We + // don't have access to the original failure reason anymore. + failInfo := []byte{ + // Fail time unknown. + 0, 0, 0, 0, 0, 0, 0, 0, + + // Zero length wire message. + 0, + + // Failure reason unknown. + 0, + + // Failure source index zero. + 0, 0, 0, 0, + } + + // Save fail info. + err = htlcBucket.Put(htlcFailInfoKey, failInfo) + if err != nil { + return err + } + } + + log.Infof("Migration of payments to mpp structure complete!") + + return nil +} diff --git a/channeldb/migration13/migration_test.go b/channeldb/migration13/migration_test.go new file mode 100644 index 00000000..4b3e4f2c --- /dev/null +++ b/channeldb/migration13/migration_test.go @@ -0,0 +1,123 @@ +package migration13 + +import ( + "testing" + + "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/channeldb/migtest" +) + +var ( + hex = migtest.Hex + + zeroTime = hex("0000000000000000") + noFailureMessage = hex("00") + failureReasonUnknown = hex("00") + zeroFailureSourceIdx = hex("00000000") + + hash1 = hex("02acee76ebd53d00824410cf6adecad4f50334dac702bd5a2d3ba01b91709f0e") + creationInfoAmt1 = hex("00000000004c4b40") + creationInfoTime1 = hex("000000005e4fb7ab") // 1582282667 (decimal) + creationInfoTimeNano1 = hex("15f565b3cccaee00") // 1582282667000000000 (decimal) + creationInfoPayReq1 = hex("00000000") + attemptInfo1 = hex("2997a72e129fc9d638ef2fa4e233567d808d4f18a4f087637582427962eb3bf800005ce600000000004c4b402102ec12e83eafe27ce6d03bbe0c0de4b79fe2b9934615c8aa7693f73d2e41b089700000000121028c2dd128c7a6c1a0fceb3e3eb5ed55e0a0ae1a939eb786b097322d830d47db75005ca4000001000000005ce600000000004c4b400000000000") + attemptID1 = hex("0000000000000001") + paymentID1 = hex("0000000000000001") + + hash2 = hex("62eb3f0a48f954e495d0c14ac63df04a67cefa59dafdbcd3d5046d1f5647840c") + preimage2 = hex("479593b7d3cbb45beb22d448451a2f3619b2095adfb38f4d92e9886e96534368") + attemptID2 = hex("00000000000003e8") + paymentID2 = hex("0000000000000002") + attemptInfo2 = hex("8de663f9bb4b8d1ebdb496d22dc1cb657a346215607308549f41b01e2adf2ce900005ce600000000005b8d802102ec12e83eafe27ce6d03bbe0c0de4b79fe2b9934615c8aa7693f73d2e41b089700000000121028c2dd128c7a6c1a0fceb3e3eb5ed55e0a0ae1a939eb786b097322d830d47db75005ca4000001000000005ce600000000005b8d8000000000010000000000000008233d281e2cbe01f0b82dd6750967c9233426b98ae6549c696365f57f86f942a3795b8d80") + creationInfoAmt2 = hex("00000000005b8d80") + creationInfoTime2 = hex("000000005e4fb97f") // 1582283135 (decimal) + creationInfoTimeNano2 = hex("15F56620C3C43600") // 1582283135000000000 (decimal) + creationInfoPayReq2 = hex("000000fc6c6e62637274363075317030796c7774367070357674346e377a6a676c39327766397773633939767630307366666e7561376a656d74376d6535373471336b3337346a387373787164717163717a70677370353835357075743937713863747374776b7735796b306a667278736e746e7a6878326a77786a636d3937346c636437327a3564757339717939717371653872336b3578733379367868667366366d6a6e706d717172306661797a677a63336a6b663571787a6c376866787a6666763578667a7679647564327275767974706571787072376868796830726a747574373033333274737774686661616e303773766b6667716b7174667275") + + hash3 = hex("62eb3f0a48f954e495d0c14ac63df04a67cefa59dafdbcd3d5046d1f5647840d") + attemptInfo3 = hex("53ce0a4c1507cc5ea00ec88b76bd43a3978ac13605497030b821af6ce9c110f300005ce600000000006acfc02102ec12e83eafe27ce6d03bbe0c0de4b79fe2b9934615c8aa7693f73d2e41b089700000000121028c2dd128c7a6c1a0fceb3e3eb5ed55e0a0ae1a939eb786b097322d830d47db75005ca4000001000000005ce600000000006acfc000000000010000000000000008233044f235354472318b381fad3e21eb5a58f5099918868b0610e7b7bcb7a4adc96acfc0") + attemptID3 = hex("00000000000003e9") + paymentID3 = hex("0000000000000003") + creationInfoAmt3 = hex("00000000006acfc0") + creationInfoTime3 = hex("000000005e4fb98d") // 1582283149 + creationInfoTimeNano3 = hex("15F56624063B4200") // 1582283149000000000 (decimal) + creationInfoPayReq3 = hex("000000fc6c6e62637274373075317030796c7776327070357674346e377a6a676c39327766397773633939767630307366666e7561376a656d74376d6535373471336b3337346a387373787364717163717a706773703578707a307964663467336572727a656372376b6e7567307474667630327a7665727a72676b70737375376d6d6564617934687973397179397173717774656479336e666c323534787a36787a75763974746767757a647473356e617a7461616a6735667772686438396b336d70753971726d7a6c3779637a306e30666e6e763077753032726632706e64636c393761646c667636376a7a6e7063677477356434366771323571326e32") + + // pre is the data in the payments root bucket in database version 12 format. + pre = map[string]interface{}{ + // A failed payment. + hash1: map[string]interface{}{ + "payment-attempt-info": attemptID1 + attemptInfo1, + "payment-creation-info": hash1 + creationInfoAmt1 + creationInfoTime1 + creationInfoPayReq1, + "payment-fail-info": hex("03"), + "payment-sequence-key": paymentID1, + }, + + // A settled payment. + hash2: map[string]interface{}{ + "payment-attempt-info": attemptID2 + attemptInfo2, + "payment-creation-info": hash2 + creationInfoAmt2 + creationInfoTime2 + creationInfoPayReq2, + "payment-sequence-key": paymentID2, + "payment-settle-info": preimage2, + }, + + // An in-flight payment. + hash3: map[string]interface{}{ + "payment-attempt-info": attemptID3 + attemptInfo3, + "payment-creation-info": hash3 + creationInfoAmt3 + creationInfoTime3 + creationInfoPayReq3, + "payment-sequence-key": paymentID3, + }, + } + + // post is the expected data after migration. + post = map[string]interface{}{ + hash1: map[string]interface{}{ + "payment-creation-info": hash1 + creationInfoAmt1 + creationInfoTimeNano1 + creationInfoPayReq1, + "payment-fail-info": hex("03"), + "payment-htlcs-bucket": map[string]interface{}{ + attemptID1: map[string]interface{}{ + "htlc-attempt-info": attemptInfo1 + zeroTime, + "htlc-fail-info": zeroTime + noFailureMessage + failureReasonUnknown + zeroFailureSourceIdx, + }, + }, + "payment-sequence-key": paymentID1, + }, + hash2: map[string]interface{}{ + "payment-creation-info": hash2 + creationInfoAmt2 + creationInfoTimeNano2 + creationInfoPayReq2, + "payment-htlcs-bucket": map[string]interface{}{ + attemptID2: map[string]interface{}{ + "htlc-attempt-info": attemptInfo2 + zeroTime, + "htlc-settle-info": preimage2 + zeroTime, + }, + }, + "payment-sequence-key": paymentID2, + }, + hash3: map[string]interface{}{ + "payment-creation-info": hash3 + creationInfoAmt3 + creationInfoTimeNano3 + creationInfoPayReq3, + "payment-htlcs-bucket": map[string]interface{}{ + attemptID3: map[string]interface{}{ + "htlc-attempt-info": attemptInfo3 + zeroTime, + }, + }, + "payment-sequence-key": paymentID3, + }, + } +) + +// TestMigrateMpp asserts that the database is properly migrated to the mpp +// payment structure. +func TestMigrateMpp(t *testing.T) { + var paymentsRootBucket = []byte("payments-root-bucket") + + migtest.ApplyMigration( + t, + func(tx *bbolt.Tx) error { + return migtest.RestoreDB(tx, paymentsRootBucket, pre) + }, + func(tx *bbolt.Tx) error { + return migtest.VerifyDB(tx, paymentsRootBucket, post) + }, + MigrateMPP, + false, + ) +}