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.
This commit is contained in:
Elle Mouton 2023-08-24 11:20:32 +02:00
parent a802759f4e
commit e90df06d58
No known key found for this signature in database
GPG key ID: D7D916376026F177
3 changed files with 243 additions and 0 deletions

View file

@ -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))

View file

@ -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 -> <key> -> <serialised session>
// -> id-index -> <session-id> -> key -> <session key>
// -> group -> <group-ID>
// -> group-id-index -> <group-id> -> session-id -> sequence -> <session-id>
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[:])
})
}

View file

@ -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[:])
}