Skip to content

Commit 75f5b40

Browse files
authored
Merge pull request #5635 from bhandras/flatten-htlc-bucket
channeldb: flatten the htlc attempts bucket
2 parents d50ee83 + f2cc783 commit 75f5b40

File tree

6 files changed

+463
-83
lines changed

6 files changed

+463
-83
lines changed

channeldb/db.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightningnetwork/lnd/channeldb/migration16"
1818
"github.com/lightningnetwork/lnd/channeldb/migration20"
1919
"github.com/lightningnetwork/lnd/channeldb/migration21"
20+
"github.com/lightningnetwork/lnd/channeldb/migration23"
2021
"github.com/lightningnetwork/lnd/channeldb/migration_01_to_11"
2122
"github.com/lightningnetwork/lnd/clock"
2223
"github.com/lightningnetwork/lnd/kvdb"
@@ -193,6 +194,10 @@ var (
193194
number: 22,
194195
migration: mig.CreateTLB(setIDIndexBucket),
195196
},
197+
{
198+
number: 23,
199+
migration: migration23.MigrateHtlcAttempts,
200+
},
196201
}
197202

198203
// Big endian is the preferred byte order, due to cursor scans over

channeldb/migration23/migration.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package migration23
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lightningnetwork/lnd/kvdb"
7+
)
8+
9+
var (
10+
// paymentsRootBucket is the name of the top-level bucket within the
11+
// database that stores all data related to payments.
12+
paymentsRootBucket = []byte("payments-root-bucket")
13+
14+
// paymentHtlcsBucket is a bucket where we'll store the information
15+
// about the HTLCs that were attempted for a payment.
16+
paymentHtlcsBucket = []byte("payment-htlcs-bucket")
17+
18+
// oldAttemptInfoKey is a key used in a HTLC's sub-bucket to store the
19+
// info about the attempt that was done for the HTLC in question.
20+
oldAttemptInfoKey = []byte("htlc-attempt-info")
21+
22+
// oldSettleInfoKey is a key used in a HTLC's sub-bucket to store the
23+
// settle info, if any.
24+
oldSettleInfoKey = []byte("htlc-settle-info")
25+
26+
// oldFailInfoKey is a key used in a HTLC's sub-bucket to store
27+
// failure information, if any.
28+
oldFailInfoKey = []byte("htlc-fail-info")
29+
30+
// htlcAttemptInfoKey is the key used as the prefix of an HTLC attempt
31+
// to store the info about the attempt that was done for the HTLC in
32+
// question. The HTLC attempt ID is concatenated at the end.
33+
htlcAttemptInfoKey = []byte("ai")
34+
35+
// htlcSettleInfoKey is the key used as the prefix of an HTLC attempt
36+
// settle info, if any. The HTLC attempt ID is concatenated at the end.
37+
htlcSettleInfoKey = []byte("si")
38+
39+
// htlcFailInfoKey is the key used as the prefix of an HTLC attempt
40+
// failure information, if any.The HTLC attempt ID is concatenated at
41+
// the end.
42+
htlcFailInfoKey = []byte("fi")
43+
)
44+
45+
// htlcBucketKey creates a composite key from prefix and id where the result is
46+
// simply the two concatenated. This is the exact copy from payments.go.
47+
func htlcBucketKey(prefix, id []byte) []byte {
48+
key := make([]byte, len(prefix)+len(id))
49+
copy(key, prefix)
50+
copy(key[len(prefix):], id)
51+
return key
52+
}
53+
54+
// MigrateHtlcAttempts will gather all htlc-attempt-info's, htlcs-settle-info's
55+
// and htlcs-fail-info's from the attempt ID buckes and re-store them using the
56+
// flattened keys to each payment's payment-htlcs-bucket.
57+
func MigrateHtlcAttempts(tx kvdb.RwTx) error {
58+
payments := tx.ReadWriteBucket(paymentsRootBucket)
59+
if payments == nil {
60+
return nil
61+
}
62+
63+
// Collect all payment hashes so we can migrate payments one-by-one to
64+
// avoid any bugs bbolt might have when invalidating cursors.
65+
// For 100 million payments, this would need about 3 GiB memory so we
66+
// should hopefully be fine for very large nodes too.
67+
var paymentHashes []string
68+
if err := payments.ForEach(func(hash, v []byte) error {
69+
// Get the bucket which contains the payment, fail if the key
70+
// does not have a bucket.
71+
bucket := payments.NestedReadBucket(hash)
72+
if bucket == nil {
73+
return fmt.Errorf("key must be a bucket: '%v'",
74+
string(paymentsRootBucket))
75+
}
76+
77+
paymentHashes = append(paymentHashes, string(hash))
78+
return nil
79+
}); err != nil {
80+
return err
81+
}
82+
83+
for _, paymentHash := range paymentHashes {
84+
payment := payments.NestedReadWriteBucket([]byte(paymentHash))
85+
if payment.Get(paymentHtlcsBucket) != nil {
86+
return fmt.Errorf("key must be a bucket: '%v'",
87+
string(paymentHtlcsBucket))
88+
}
89+
90+
htlcs := payment.NestedReadWriteBucket(paymentHtlcsBucket)
91+
if htlcs == nil {
92+
// Nothing to migrate for this payment.
93+
continue
94+
}
95+
96+
if err := migrateHtlcsBucket(htlcs); err != nil {
97+
return err
98+
}
99+
}
100+
101+
return nil
102+
}
103+
104+
// migrateHtlcsBucket is a helper to gather, transform and re-store htlc attempt
105+
// key/values.
106+
func migrateHtlcsBucket(htlcs kvdb.RwBucket) error {
107+
// Collect attempt ids so that we can migrate attempts one-by-one
108+
// to avoid any bugs bbolt might have when invalidating cursors.
109+
var aids []string
110+
111+
// First we collect all htlc attempt ids.
112+
if err := htlcs.ForEach(func(aid, v []byte) error {
113+
aids = append(aids, string(aid))
114+
return nil
115+
}); err != nil {
116+
return err
117+
}
118+
119+
// Next we go over these attempts, fetch all data and migrate.
120+
for _, aid := range aids {
121+
aidKey := []byte(aid)
122+
attempt := htlcs.NestedReadWriteBucket(aidKey)
123+
if attempt == nil {
124+
return fmt.Errorf("non bucket element '%v' in '%v' "+
125+
"bucket", aidKey, string(paymentHtlcsBucket))
126+
}
127+
128+
// Collect attempt/settle/fail infos.
129+
attemptInfo := attempt.Get(oldAttemptInfoKey)
130+
if len(attemptInfo) > 0 {
131+
newKey := htlcBucketKey(htlcAttemptInfoKey, aidKey)
132+
if err := htlcs.Put(newKey, attemptInfo); err != nil {
133+
return err
134+
}
135+
}
136+
137+
settleInfo := attempt.Get(oldSettleInfoKey)
138+
if len(settleInfo) > 0 {
139+
newKey := htlcBucketKey(htlcSettleInfoKey, aidKey)
140+
if err := htlcs.Put(newKey, settleInfo); err != nil {
141+
return err
142+
}
143+
144+
}
145+
146+
failInfo := attempt.Get(oldFailInfoKey)
147+
if len(failInfo) > 0 {
148+
newKey := htlcBucketKey(htlcFailInfoKey, aidKey)
149+
if err := htlcs.Put(newKey, failInfo); err != nil {
150+
return err
151+
}
152+
}
153+
}
154+
155+
// Finally we delete old attempt buckets.
156+
for _, aid := range aids {
157+
if err := htlcs.DeleteNestedBucket([]byte(aid)); err != nil {
158+
return err
159+
}
160+
}
161+
162+
return nil
163+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package migration23
2+
3+
import (
4+
"testing"
5+
6+
"github.com/lightningnetwork/lnd/channeldb/migtest"
7+
"github.com/lightningnetwork/lnd/kvdb"
8+
)
9+
10+
var (
11+
hexStr = migtest.Hex
12+
13+
hash1Str = "02acee76ebd53d00824410cf6adecad4f50334dac702bd5a2d3ba01b91709f0e"
14+
hash1 = hexStr(hash1Str)
15+
paymentID1 = hexStr("0000000000000001")
16+
attemptID1 = hexStr("0000000000000001")
17+
attemptID2 = hexStr("0000000000000002")
18+
19+
hash2Str = "62eb3f0a48f954e495d0c14ac63df04a67cefa59dafdbcd3d5046d1f5647840c"
20+
hash2 = hexStr(hash2Str)
21+
paymentID2 = hexStr("0000000000000002")
22+
attemptID3 = hexStr("0000000000000003")
23+
24+
hash3Str = "99eb3f0a48f954e495d0c14ac63df04af8cefa59dafdbcd3d5046d1f564784d1"
25+
hash3 = hexStr(hash3Str)
26+
27+
// failing1 will fail because all payment hashes should point to sub
28+
// buckets containing payment data.
29+
failing1 = map[string]interface{}{
30+
hash1: "bogus",
31+
}
32+
33+
// failing2 will fail because the "payment-htlcs-bucket" key must point
34+
// to an actual bucket or be non-existent, but never point to a value.
35+
failing2 = map[string]interface{}{
36+
hash1: map[string]interface{}{
37+
"payment-htlcs-bucket": "bogus",
38+
},
39+
}
40+
41+
// failing3 will fail because each attempt ID inside the
42+
// "payment-htlcs-bucket" must point to a sub-bucket.
43+
failing3 = map[string]interface{}{
44+
hash1: map[string]interface{}{
45+
"payment-creation-info": "aaaa",
46+
"payment-fail-info": "bbbb",
47+
"payment-htlcs-bucket": map[string]interface{}{
48+
attemptID1: map[string]interface{}{
49+
"htlc-attempt-info": "cccc",
50+
"htlc-fail-info": "dddd",
51+
},
52+
attemptID2: "bogus",
53+
},
54+
"payment-sequence-key": paymentID1,
55+
},
56+
}
57+
58+
// pre is a sample snapshot (with fake values) before migration.
59+
pre = map[string]interface{}{
60+
hash1: map[string]interface{}{
61+
"payment-creation-info": "aaaa",
62+
"payment-fail-info": "bbbb",
63+
"payment-htlcs-bucket": map[string]interface{}{
64+
attemptID1: map[string]interface{}{
65+
"htlc-attempt-info": "cccc",
66+
"htlc-fail-info": "dddd",
67+
},
68+
},
69+
"payment-sequence-key": paymentID1,
70+
},
71+
hash2: map[string]interface{}{
72+
"payment-creation-info": "eeee",
73+
"payment-htlcs-bucket": map[string]interface{}{
74+
attemptID2: map[string]interface{}{
75+
"htlc-attempt-info": "ffff",
76+
"htlc-fail-info": "gggg",
77+
},
78+
attemptID3: map[string]interface{}{
79+
"htlc-attempt-info": "hhhh",
80+
"htlc-settle-info": "iiii",
81+
},
82+
},
83+
"payment-sequence-key": paymentID2,
84+
},
85+
hash3: map[string]interface{}{
86+
"payment-creation-info": "aaaa",
87+
"payment-fail-info": "bbbb",
88+
"payment-sequence-key": paymentID1,
89+
},
90+
}
91+
92+
// post is the expected data after migration.
93+
post = map[string]interface{}{
94+
hash1: map[string]interface{}{
95+
"payment-creation-info": "aaaa",
96+
"payment-fail-info": "bbbb",
97+
"payment-htlcs-bucket": map[string]interface{}{
98+
"ai" + attemptID1: "cccc",
99+
"fi" + attemptID1: "dddd",
100+
},
101+
"payment-sequence-key": paymentID1,
102+
},
103+
hash2: map[string]interface{}{
104+
"payment-creation-info": "eeee",
105+
"payment-htlcs-bucket": map[string]interface{}{
106+
"ai" + attemptID2: "ffff",
107+
"fi" + attemptID2: "gggg",
108+
"ai" + attemptID3: "hhhh",
109+
"si" + attemptID3: "iiii",
110+
},
111+
"payment-sequence-key": paymentID2,
112+
},
113+
hash3: map[string]interface{}{
114+
"payment-creation-info": "aaaa",
115+
"payment-fail-info": "bbbb",
116+
"payment-sequence-key": paymentID1,
117+
},
118+
}
119+
)
120+
121+
// TestMigrateHtlcAttempts tests that migration htlc attempts to the flattened
122+
// structure succeeds.
123+
func TestMigrateHtlcAttempts(t *testing.T) {
124+
var paymentsRootBucket = []byte("payments-root-bucket")
125+
tests := []struct {
126+
name string
127+
shouldFail bool
128+
pre map[string]interface{}
129+
post map[string]interface{}
130+
}{
131+
{
132+
name: "migration ok",
133+
shouldFail: false,
134+
pre: pre,
135+
post: post,
136+
},
137+
{
138+
name: "non-bucket payments-root-bucket",
139+
shouldFail: true,
140+
pre: failing1,
141+
post: failing1,
142+
},
143+
{
144+
name: "non-bucket payment-htlcs-bucket",
145+
shouldFail: true,
146+
pre: failing2,
147+
post: failing2,
148+
},
149+
{
150+
name: "non-bucket htlc attempt",
151+
shouldFail: true,
152+
pre: failing3,
153+
post: failing3,
154+
},
155+
}
156+
157+
for _, test := range tests {
158+
test := test
159+
160+
migtest.ApplyMigration(
161+
t,
162+
func(tx kvdb.RwTx) error {
163+
return migtest.RestoreDB(
164+
tx, paymentsRootBucket, test.pre,
165+
)
166+
},
167+
func(tx kvdb.RwTx) error {
168+
return migtest.VerifyDB(
169+
tx, paymentsRootBucket, test.post,
170+
)
171+
},
172+
MigrateHtlcAttempts,
173+
test.shouldFail,
174+
)
175+
}
176+
}

0 commit comments

Comments
 (0)