diff --git a/channeldb/db.go b/channeldb/db.go index 530c819a1..5a7b59b63 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -17,6 +17,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/migration16" "github.com/lightningnetwork/lnd/channeldb/migration20" "github.com/lightningnetwork/lnd/channeldb/migration21" + "github.com/lightningnetwork/lnd/channeldb/migration23" "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/kvdb" @@ -193,6 +194,10 @@ var ( number: 22, migration: mig.CreateTLB(setIDIndexBucket), }, + { + number: 23, + migration: migration23.MigrateHtlcAttempts, + }, } // Big endian is the preferred byte order, due to cursor scans over diff --git a/channeldb/migration23/migration.go b/channeldb/migration23/migration.go new file mode 100644 index 000000000..7e48791ec --- /dev/null +++ b/channeldb/migration23/migration.go @@ -0,0 +1,163 @@ +package migration23 + +import ( + "fmt" + + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // paymentsRootBucket is the name of the top-level bucket within the + // database that stores all data related to payments. + paymentsRootBucket = []byte("payments-root-bucket") + + // paymentHtlcsBucket is a bucket where we'll store the information + // about the HTLCs that were attempted for a payment. + paymentHtlcsBucket = []byte("payment-htlcs-bucket") + + // oldAttemptInfoKey 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. + oldAttemptInfoKey = []byte("htlc-attempt-info") + + // oldSettleInfoKey is a key used in a HTLC's sub-bucket to store the + // settle info, if any. + oldSettleInfoKey = []byte("htlc-settle-info") + + // oldFailInfoKey is a key used in a HTLC's sub-bucket to store + // failure information, if any. + oldFailInfoKey = []byte("htlc-fail-info") + + // htlcAttemptInfoKey is the key used as the prefix of an HTLC attempt + // to store the info about the attempt that was done for the HTLC in + // question. The HTLC attempt ID is concatenated at the end. + htlcAttemptInfoKey = []byte("ai") + + // htlcSettleInfoKey is the key used as the prefix of an HTLC attempt + // settle info, if any. The HTLC attempt ID is concatenated at the end. + htlcSettleInfoKey = []byte("si") + + // htlcFailInfoKey is the key used as the prefix of an HTLC attempt + // failure information, if any.The HTLC attempt ID is concatenated at + // the end. + htlcFailInfoKey = []byte("fi") +) + +// htlcBucketKey creates a composite key from prefix and id where the result is +// simply the two concatenated. This is the exact copy from payments.go. +func htlcBucketKey(prefix, id []byte) []byte { + key := make([]byte, len(prefix)+len(id)) + copy(key, prefix) + copy(key[len(prefix):], id) + return key +} + +// MigrateHtlcAttempts will gather all htlc-attempt-info's, htlcs-settle-info's +// and htlcs-fail-info's from the attempt ID buckes and re-store them using the +// flattened keys to each payment's payment-htlcs-bucket. +func MigrateHtlcAttempts(tx kvdb.RwTx) error { + payments := tx.ReadWriteBucket(paymentsRootBucket) + if payments == nil { + return nil + } + + // Collect all payment hashes so we can migrate payments one-by-one to + // avoid any bugs bbolt might have when invalidating cursors. + // For 100 million payments, this would need about 3 GiB memory so we + // should hopefully be fine for very large nodes too. + var paymentHashes []string + if err := payments.ForEach(func(hash, v []byte) error { + // Get the bucket which contains the payment, fail if the key + // does not have a bucket. + bucket := payments.NestedReadBucket(hash) + if bucket == nil { + return fmt.Errorf("key must be a bucket: '%v'", + string(paymentsRootBucket)) + } + + paymentHashes = append(paymentHashes, string(hash)) + return nil + }); err != nil { + return err + } + + for _, paymentHash := range paymentHashes { + payment := payments.NestedReadWriteBucket([]byte(paymentHash)) + if payment.Get(paymentHtlcsBucket) != nil { + return fmt.Errorf("key must be a bucket: '%v'", + string(paymentHtlcsBucket)) + } + + htlcs := payment.NestedReadWriteBucket(paymentHtlcsBucket) + if htlcs == nil { + // Nothing to migrate for this payment. + continue + } + + if err := migrateHtlcsBucket(htlcs); err != nil { + return err + } + } + + return nil +} + +// migrateHtlcsBucket is a helper to gather, transform and re-store htlc attempt +// key/values. +func migrateHtlcsBucket(htlcs kvdb.RwBucket) error { + // Collect attempt ids so that we can migrate attempts one-by-one + // to avoid any bugs bbolt might have when invalidating cursors. + var aids []string + + // First we collect all htlc attempt ids. + if err := htlcs.ForEach(func(aid, v []byte) error { + aids = append(aids, string(aid)) + return nil + }); err != nil { + return err + } + + // Next we go over these attempts, fetch all data and migrate. + for _, aid := range aids { + aidKey := []byte(aid) + attempt := htlcs.NestedReadWriteBucket(aidKey) + if attempt == nil { + return fmt.Errorf("non bucket element '%v' in '%v' "+ + "bucket", aidKey, string(paymentHtlcsBucket)) + } + + // Collect attempt/settle/fail infos. + attemptInfo := attempt.Get(oldAttemptInfoKey) + if len(attemptInfo) > 0 { + newKey := htlcBucketKey(htlcAttemptInfoKey, aidKey) + if err := htlcs.Put(newKey, attemptInfo); err != nil { + return err + } + } + + settleInfo := attempt.Get(oldSettleInfoKey) + if len(settleInfo) > 0 { + newKey := htlcBucketKey(htlcSettleInfoKey, aidKey) + if err := htlcs.Put(newKey, settleInfo); err != nil { + return err + } + + } + + failInfo := attempt.Get(oldFailInfoKey) + if len(failInfo) > 0 { + newKey := htlcBucketKey(htlcFailInfoKey, aidKey) + if err := htlcs.Put(newKey, failInfo); err != nil { + return err + } + } + } + + // Finally we delete old attempt buckets. + for _, aid := range aids { + if err := htlcs.DeleteNestedBucket([]byte(aid)); err != nil { + return err + } + } + + return nil +} diff --git a/channeldb/migration23/migration_test.go b/channeldb/migration23/migration_test.go new file mode 100644 index 000000000..350c4cc91 --- /dev/null +++ b/channeldb/migration23/migration_test.go @@ -0,0 +1,176 @@ +package migration23 + +import ( + "testing" + + "github.com/lightningnetwork/lnd/channeldb/migtest" + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + hexStr = migtest.Hex + + hash1Str = "02acee76ebd53d00824410cf6adecad4f50334dac702bd5a2d3ba01b91709f0e" + hash1 = hexStr(hash1Str) + paymentID1 = hexStr("0000000000000001") + attemptID1 = hexStr("0000000000000001") + attemptID2 = hexStr("0000000000000002") + + hash2Str = "62eb3f0a48f954e495d0c14ac63df04a67cefa59dafdbcd3d5046d1f5647840c" + hash2 = hexStr(hash2Str) + paymentID2 = hexStr("0000000000000002") + attemptID3 = hexStr("0000000000000003") + + hash3Str = "99eb3f0a48f954e495d0c14ac63df04af8cefa59dafdbcd3d5046d1f564784d1" + hash3 = hexStr(hash3Str) + + // failing1 will fail because all payment hashes should point to sub + // buckets containing payment data. + failing1 = map[string]interface{}{ + hash1: "bogus", + } + + // failing2 will fail because the "payment-htlcs-bucket" key must point + // to an actual bucket or be non-existent, but never point to a value. + failing2 = map[string]interface{}{ + hash1: map[string]interface{}{ + "payment-htlcs-bucket": "bogus", + }, + } + + // failing3 will fail because each attempt ID inside the + // "payment-htlcs-bucket" must point to a sub-bucket. + failing3 = map[string]interface{}{ + hash1: map[string]interface{}{ + "payment-creation-info": "aaaa", + "payment-fail-info": "bbbb", + "payment-htlcs-bucket": map[string]interface{}{ + attemptID1: map[string]interface{}{ + "htlc-attempt-info": "cccc", + "htlc-fail-info": "dddd", + }, + attemptID2: "bogus", + }, + "payment-sequence-key": paymentID1, + }, + } + + // pre is a sample snapshot (with fake values) before migration. + pre = map[string]interface{}{ + hash1: map[string]interface{}{ + "payment-creation-info": "aaaa", + "payment-fail-info": "bbbb", + "payment-htlcs-bucket": map[string]interface{}{ + attemptID1: map[string]interface{}{ + "htlc-attempt-info": "cccc", + "htlc-fail-info": "dddd", + }, + }, + "payment-sequence-key": paymentID1, + }, + hash2: map[string]interface{}{ + "payment-creation-info": "eeee", + "payment-htlcs-bucket": map[string]interface{}{ + attemptID2: map[string]interface{}{ + "htlc-attempt-info": "ffff", + "htlc-fail-info": "gggg", + }, + attemptID3: map[string]interface{}{ + "htlc-attempt-info": "hhhh", + "htlc-settle-info": "iiii", + }, + }, + "payment-sequence-key": paymentID2, + }, + hash3: map[string]interface{}{ + "payment-creation-info": "aaaa", + "payment-fail-info": "bbbb", + "payment-sequence-key": paymentID1, + }, + } + + // post is the expected data after migration. + post = map[string]interface{}{ + hash1: map[string]interface{}{ + "payment-creation-info": "aaaa", + "payment-fail-info": "bbbb", + "payment-htlcs-bucket": map[string]interface{}{ + "ai" + attemptID1: "cccc", + "fi" + attemptID1: "dddd", + }, + "payment-sequence-key": paymentID1, + }, + hash2: map[string]interface{}{ + "payment-creation-info": "eeee", + "payment-htlcs-bucket": map[string]interface{}{ + "ai" + attemptID2: "ffff", + "fi" + attemptID2: "gggg", + "ai" + attemptID3: "hhhh", + "si" + attemptID3: "iiii", + }, + "payment-sequence-key": paymentID2, + }, + hash3: map[string]interface{}{ + "payment-creation-info": "aaaa", + "payment-fail-info": "bbbb", + "payment-sequence-key": paymentID1, + }, + } +) + +// TestMigrateHtlcAttempts tests that migration htlc attempts to the flattened +// structure succeeds. +func TestMigrateHtlcAttempts(t *testing.T) { + var paymentsRootBucket = []byte("payments-root-bucket") + tests := []struct { + name string + shouldFail bool + pre map[string]interface{} + post map[string]interface{} + }{ + { + name: "migration ok", + shouldFail: false, + pre: pre, + post: post, + }, + { + name: "non-bucket payments-root-bucket", + shouldFail: true, + pre: failing1, + post: failing1, + }, + { + name: "non-bucket payment-htlcs-bucket", + shouldFail: true, + pre: failing2, + post: failing2, + }, + { + name: "non-bucket htlc attempt", + shouldFail: true, + pre: failing3, + post: failing3, + }, + } + + for _, test := range tests { + test := test + + migtest.ApplyMigration( + t, + func(tx kvdb.RwTx) error { + return migtest.RestoreDB( + tx, paymentsRootBucket, test.pre, + ) + }, + func(tx kvdb.RwTx) error { + return migtest.VerifyDB( + tx, paymentsRootBucket, test.post, + ) + }, + MigrateHtlcAttempts, + test.shouldFail, + ) + } +}