Skip to content

Commit 0e3eba8

Browse files
session: add deterministic map compare
This commit adds a deterministic comparison for the maps in sessions during the KVDB to SQL migration.
1 parent 3b51680 commit 0e3eba8

File tree

1 file changed

+109
-4
lines changed

1 file changed

+109
-4
lines changed

session/sql_migration.go

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import (
1010
"sort"
1111
"time"
1212

13+
"github.com/btcsuite/btcd/btcec/v2"
1314
"github.com/davecgh/go-spew/spew"
15+
"github.com/lightninglabs/lightning-node-connect/mailbox"
1416
"github.com/lightninglabs/lightning-terminal/accounts"
1517
"github.com/lightninglabs/lightning-terminal/db/sqlc"
18+
"github.com/lightningnetwork/lnd/fn"
1619
"github.com/lightningnetwork/lnd/sqldb"
1720
"github.com/pmezard/go-difflib/difflib"
1821
"go.etcd.io/bbolt"
@@ -25,6 +28,105 @@ var (
2528
"original session")
2629
)
2730

31+
// featureConfigEntry is a variant of a single session feature config, which
32+
// can be inserted into an array to be compared deterministically.
33+
type featureConfigEntry struct {
34+
featureName string
35+
config []byte
36+
}
37+
38+
// deterministicSession is a variant of the Session struct without any struct
39+
// methods, which represents the map in the Session as a list, so that it can be
40+
// deterministically sorted for comparison during the kvdb to SQL migration.
41+
type deterministicSession struct {
42+
ID ID
43+
Label string
44+
State State
45+
Type Type
46+
Expiry time.Time
47+
CreatedAt time.Time
48+
RevokedAt time.Time
49+
ServerAddr string
50+
DevServer bool
51+
MacaroonRootKey uint64
52+
MacaroonRecipe *MacaroonRecipe
53+
PairingSecret [mailbox.NumPassphraseEntropyBytes]byte
54+
LocalPrivateKey *btcec.PrivateKey
55+
LocalPublicKey *btcec.PublicKey
56+
RemotePublicKey *btcec.PublicKey
57+
FeatureConfig []*featureConfigEntry
58+
WithPrivacyMapper bool
59+
PrivacyFlags PrivacyFlags
60+
61+
// GroupID is the Session ID of the very first Session in the linked
62+
// group of sessions. If this is the very first session in the group
63+
// then this will be the same as ID.
64+
GroupID ID
65+
66+
// AccountID is an optional account that the session has been linked to.
67+
AccountID fn.Option[accounts.AccountID]
68+
}
69+
70+
// newDeterministicSession creates a deterministicSession from a Session struct.
71+
// This is used to compare the session in a deterministic way during the
72+
// migration from the KV database to the SQL database.
73+
func newDeterministicSession(sess *Session) *deterministicSession {
74+
var featuresConfig []*featureConfigEntry
75+
76+
// If a session has a feature config set, we'll convert it to an array
77+
// so that we can sort it and compare it deterministically.
78+
if sess.FeatureConfig != nil {
79+
sessFC := *sess.FeatureConfig
80+
featuresConfig = make([]*featureConfigEntry, len(sessFC))
81+
82+
i := 0
83+
for featureName, config := range sessFC {
84+
featuresConfig[i] = &featureConfigEntry{
85+
featureName: featureName,
86+
config: config,
87+
}
88+
89+
i++
90+
}
91+
92+
// Sort the feature config entries by their feature name, and
93+
// by their config bytes if the feature names are the same.
94+
sort.Slice(featuresConfig, func(i, j int) bool {
95+
iC := featuresConfig[i]
96+
jC := featuresConfig[j]
97+
98+
if iC.featureName == jC.featureName {
99+
return bytes.Compare(iC.config, jC.config) < 0
100+
}
101+
102+
return iC.featureName < jC.featureName
103+
})
104+
}
105+
106+
return &deterministicSession{
107+
ID: sess.ID,
108+
Label: sess.Label,
109+
State: sess.State,
110+
Type: sess.Type,
111+
Expiry: sess.Expiry,
112+
CreatedAt: sess.CreatedAt,
113+
RevokedAt: sess.RevokedAt,
114+
ServerAddr: sess.ServerAddr,
115+
DevServer: sess.DevServer,
116+
MacaroonRootKey: sess.MacaroonRootKey,
117+
PairingSecret: sess.PairingSecret,
118+
LocalPrivateKey: sess.LocalPrivateKey,
119+
LocalPublicKey: sess.LocalPublicKey,
120+
RemotePublicKey: sess.RemotePublicKey,
121+
GroupID: sess.GroupID,
122+
AccountID: sess.AccountID,
123+
PrivacyFlags: sess.PrivacyFlags,
124+
MacaroonRecipe: sess.MacaroonRecipe,
125+
WithPrivacyMapper: sess.WithPrivacyMapper,
126+
FeatureConfig: featuresConfig,
127+
}
128+
}
129+
28130
// MigrateSessionStoreToSQL runs the migration of all sessions from the KV
29131
// database to the SQL database. The migration is done in a single transaction
30132
// to ensure that all sessions are migrated or none at all.
@@ -149,13 +251,16 @@ func migrateSessionsToSQLAndValidate(ctx context.Context,
149251
overrideSessionTimeZone(migratedSession)
150252
overrideMacaroonRecipe(kvSession, migratedSession)
151253

152-
if !reflect.DeepEqual(kvSession, migratedSession) {
254+
dKvSession := newDeterministicSession(kvSession)
255+
dMigratedSession := newDeterministicSession(migratedSession)
256+
257+
if !reflect.DeepEqual(dKvSession, dMigratedSession) {
153258
diff := difflib.UnifiedDiff{
154259
A: difflib.SplitLines(
155-
spew.Sdump(kvSession),
260+
spew.Sdump(dKvSession),
156261
),
157262
B: difflib.SplitLines(
158-
spew.Sdump(migratedSession),
263+
spew.Sdump(dMigratedSession),
159264
),
160265
FromFile: "Expected",
161266
FromDate: "",
@@ -166,7 +271,7 @@ func migrateSessionsToSQLAndValidate(ctx context.Context,
166271
diffText, _ := difflib.GetUnifiedDiffString(diff)
167272

168273
return fmt.Errorf("%w: %v.\n%v", ErrMigrationMismatch,
169-
kvSession.ID, diffText)
274+
dKvSession.ID, diffText)
170275
}
171276
}
172277

0 commit comments

Comments
 (0)