From e90df06d587230c511ea9a85224e34c978b404b5 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 24 Aug 2023 11:20:32 +0200 Subject: [PATCH] session: migration to populate group ID to session ID indexes This commit adds a migration to the session store to back-fill the new session ID to group ID and group ID to session IDs indexes. --- session/metadata.go | 2 + session/migration2/id_to_group_index.go | 119 ++++++++++++++++++ session/migration2/id_to_group_index_test.go | 122 +++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 session/migration2/id_to_group_index.go create mode 100644 session/migration2/id_to_group_index_test.go diff --git a/session/metadata.go b/session/metadata.go index 24575c68..407fad3b 100644 --- a/session/metadata.go +++ b/session/metadata.go @@ -6,6 +6,7 @@ import ( "time" "github.com/lightninglabs/lightning-terminal/session/migration1" + "github.com/lightninglabs/lightning-terminal/session/migration2" "go.etcd.io/bbolt" ) @@ -37,6 +38,7 @@ var ( tx, time.Now, ) }, + migration2.MigrateSessionIDToGroupIndex, } latestDBVersion = uint32(len(dbVersions)) diff --git a/session/migration2/id_to_group_index.go b/session/migration2/id_to_group_index.go new file mode 100644 index 00000000..382a352f --- /dev/null +++ b/session/migration2/id_to_group_index.go @@ -0,0 +1,119 @@ +package migration2 + +import ( + "encoding/binary" + "errors" + "fmt" + + "go.etcd.io/bbolt" +) + +var ( + // sessionBucketKey is the top level bucket where we can find all + // information about sessions. These sessions are indexed by their + // public key. + // + // The session bucket has the following structure: + // session -> -> + // -> id-index -> -> key -> + // -> group -> + // -> group-id-index -> -> session-id -> sequence -> + sessionBucketKey = []byte("session") + + // idIndexKey is the key used to define the id-index sub-bucket within + // the main session bucket. This bucket will be used to store the + // mapping from session ID to various other fields. + idIndexKey = []byte("id-index") + + // sessionKeyKey is the key used within the id-index bucket to store the + // session key (serialised local public key) associated with the given + // session ID. + sessionKeyKey = []byte("key") + + // groupIDKey is the key used within the id-index bucket to store the + // group ID associated with the given session ID. + groupIDKey = []byte("group") + + // groupIDIndexKey is the key used to define the group-id-index + // sub-bucket within the main session bucket. This bucket will be used + // to store the mapping from group ID to various other fields. + groupIDIndexKey = []byte("group-id-index") + + // sessionIDKey is a key used in the group-id-index under a sub-bucket + // defined by a specific group ID. It will be used to store the session + // IDs associated with the given group ID. + sessionIDKey = []byte("session-id") + + // ErrDBInitErr is returned when a bucket that we expect to have been + // set up during DB initialisation is not found. + ErrDBInitErr = errors.New("db did not initialise properly") + + // byteOrder is the default byte order we'll use for serialization + // within the database. + byteOrder = binary.BigEndian +) + +// MigrateSessionIDToGroupIndex back-fills the session ID to group index so that +// it has an entry for all sessions that the session store is currently aware of. +func MigrateSessionIDToGroupIndex(tx *bbolt.Tx) error { + sessionBucket := tx.Bucket(sessionBucketKey) + if sessionBucket == nil { + return fmt.Errorf("session bucket not found") + } + + idIndexBkt := sessionBucket.Bucket(idIndexKey) + if idIndexBkt == nil { + return ErrDBInitErr + } + + groupIndexBkt := sessionBucket.Bucket(groupIDIndexKey) + if groupIndexBkt == nil { + return ErrDBInitErr + } + + // Collect all the index entries. + return idIndexBkt.ForEach(func(sessionID, _ []byte) error { + // This migration is done before the logic in LiT is added that + // would allow groupIDs to differ from session IDs. And so all + // this migration needs to do is add the current 1:1 mapping + // from group ID to session ID and vice versa where group ID is + // equal to the session ID. + groupID := sessionID + + // First we add the session ID to group ID mapping. + sessionIDBkt := idIndexBkt.Bucket(sessionID) + if sessionIDBkt == nil { + return fmt.Errorf("unexpected non-bucket entry in " + + "the id-index bucket") + } + + err := sessionIDBkt.Put(groupIDKey, groupID) + if err != nil { + return err + } + + // Now we will add the group ID to session ID mapping. + groupIDBkt, err := groupIndexBkt.CreateBucketIfNotExists( + groupID, + ) + if err != nil { + return err + } + + groupSessionIDBkt, err := groupIDBkt.CreateBucketIfNotExists( + sessionIDKey, + ) + if err != nil { + return err + } + + nextSeq, err := groupSessionIDBkt.NextSequence() + if err != nil { + return err + } + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSeq) + + return groupSessionIDBkt.Put(seqNoBytes[:], groupID[:]) + }) +} diff --git a/session/migration2/id_to_group_index_test.go b/session/migration2/id_to_group_index_test.go new file mode 100644 index 00000000..c2035216 --- /dev/null +++ b/session/migration2/id_to_group_index_test.go @@ -0,0 +1,122 @@ +package migration2 + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-terminal/session/migtest" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +// ID represents the id of a session. +type ID [4]byte + +// TestMigrateSessionIDToGroupIDIndex tests that the +// MigrateSessionIDToGroupIDIndex migration correctly back-fills the session ID +// to group ID index along with the group ID to session ID index. +func TestMigrateSessionIDToGroupIDIndex(t *testing.T) { + t.Parallel() + + // Make a few session IDs. + sess1ID, sess1Key := newSessionID(t) + sess2ID, sess2Key := newSessionID(t) + sess3ID, sess3Key := newSessionID(t) + + // Put together a sample session ID index DB based on the above. + idIndexBefore := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess1Key), + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess2Key), + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess3Key), + }, + } + + // sessionDBBefore is what our session DB will look like before the + // migration. + sessionDBBefore := map[string]interface{}{ + string(idIndexKey): idIndexBefore, + string(groupIDIndexKey): map[string]interface{}{}, + } + + before := func(tx *bbolt.Tx) error { + return migtest.RestoreDB(tx, sessionBucketKey, sessionDBBefore) + } + + // Put together what we expect the resulting id-index bucket to look + // like after the migration. + idIndexAfter := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess1Key), + string(groupIDKey): string(sess1ID[:]), + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess2Key), + string(groupIDKey): string(sess2ID[:]), + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionKeyKey): string(sess3Key), + string(groupIDKey): string(sess3ID[:]), + }, + } + + // Put together what we expect the resulting group-ID-index bucket to + // look like after the migration. + groupIDIndexAfter := map[string]interface{}{ + string(sess1ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess1ID[:]), + }, + }, + string(sess2ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess2ID[:]), + }, + }, + string(sess3ID[:]): map[string]interface{}{ + string(sessionIDKey): map[string]interface{}{ + sequenceString(1): string(sess3ID[:]), + }, + }, + } + + // sessionDBAfter is what our session DB will look like after the + // migration. + sessionDBAfter := map[string]interface{}{ + string(idIndexKey): idIndexAfter, + string(groupIDIndexKey): groupIDIndexAfter, + } + + after := func(tx *bbolt.Tx) error { + return migtest.VerifyDB(tx, sessionBucketKey, sessionDBAfter) + } + + migtest.ApplyMigration( + t, before, after, MigrateSessionIDToGroupIndex, false, + ) +} + +// newSessionID is a helper function that can be used to generate a new session +// ID and key. +func newSessionID(t *testing.T) (ID, []byte) { + privateKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + key := privateKey.PubKey().SerializeCompressed() + + var id ID + copy(id[:], key) + + return id, key +} + +func sequenceString(id uint64) string { + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], id) + + return string(seqNoBytes[:]) +}