From 059554474e92b102dbbaeda5ee6e87c5fdf2ebd8 Mon Sep 17 00:00:00 2001 From: Viktor Torstensson Date: Thu, 11 Jun 2026 23:04:37 +0200 Subject: [PATCH] db: skip SQL migration when no kvdb files exist Avoid creating legacy bbolt database files when the kvdb-to-SQL programmatic migration runs on a node that never had LiT kvdb state. Note that we only skip the creating the database file if there is no single bbolt database file. If for example there's only 1 existing bbolt database file, we will still create the new bbolt database file for the other 2 bbolt databases, as we for example need to pass the accounts & session databases to the firewalldb migration, and we'd want to avoid having the migrations become more complex by having to handle the case where the other databases are missing. --- db/migsets/programmatic_migrations.go | 36 +++++ db/migsets/programmatic_migrations_test.go | 152 +++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 db/migsets/programmatic_migrations_test.go diff --git a/db/migsets/programmatic_migrations.go b/db/migsets/programmatic_migrations.go index 70825375..585bf016 100644 --- a/db/migsets/programmatic_migrations.go +++ b/db/migsets/programmatic_migrations.go @@ -6,12 +6,14 @@ import ( "encoding/binary" "errors" "fmt" + "path/filepath" "time" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database" "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/db/sqlcmig6" + "github.com/lightninglabs/lightning-terminal/db/tombstone" "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" @@ -88,6 +90,40 @@ func kvdbToSqlProgrammaticMigration(ctx context.Context, _ *sqldb.BaseDB, clock clock.Clock, q *sqlcmig6.Queries) error { start := time.Now() + + accountsActive, err := tombstone.KVDBFileExists( + filepath.Join(accountsDir, accounts.DBFilename), + ) + if err != nil { + return fmt.Errorf("unable to inspect accounts kvdb: %w", err) + } + + sessionsActive, err := tombstone.KVDBFileExists( + filepath.Join(networkDir, session.DBFilename), + ) + if err != nil { + return fmt.Errorf("unable to inspect session kvdb: %w", err) + } + + firewallActive, err := tombstone.KVDBFileExists( + filepath.Join(networkDir, firewalldb.DBFilename), + ) + if err != nil { + return fmt.Errorf("unable to inspect rules kvdb: %w", err) + } + + if !accountsActive && !sessionsActive && !firewallActive { + log.Infof("Skipping KVDB to SQL migration for all stores: " + + "no legacy database files exist") + + return nil + } + + if basicClient == nil { + return errors.New("lightning client is required for " + + "migration but was nil") + } + log.Infof("Starting KVDB to SQL migration for all stores") accountStore, err := accounts.NewBoltStoreForMigration( diff --git a/db/migsets/programmatic_migrations_test.go b/db/migsets/programmatic_migrations_test.go new file mode 100644 index 00000000..49d3b110 --- /dev/null +++ b/db/migsets/programmatic_migrations_test.go @@ -0,0 +1,152 @@ +package migsets + +import ( + "context" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/lightninglabs/lightning-terminal/accounts" + "github.com/lightninglabs/lightning-terminal/db" + "github.com/lightninglabs/lightning-terminal/db/sqlcmig6" + "github.com/lightninglabs/lightning-terminal/firewalldb" + "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/sqldb/v2" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +// TestKVDBToSQLProgrammaticMigrationSkipsMissingStores verifies that the kvdb +// to SQL migration does not create missing legacy kvdb files while scanning for +// stores to migrate. +func TestKVDBToSQLProgrammaticMigrationSkipsMissingStores(t *testing.T) { + t.Parallel() + + accountsDir := t.TempDir() + networkDir := t.TempDir() + + sqlStore := sqldb.NewTestSqliteDB(t, db.MakeTestMigrationSets()) + queries := sqlcmig6.NewForType( + sqlStore.BaseDB, sqlStore.BackendType, + ) + + err := kvdbToSqlProgrammaticMigration( + context.Background(), nil, accountsDir, networkDir, + sqlStore.BaseDB, clock.NewDefaultClock(), queries, + ) + require.NoError(t, err) + + requireNoFile(t, filepath.Join(accountsDir, accounts.DBFilename)) + requireNoFile(t, filepath.Join(networkDir, session.DBFilename)) + requireNoFile(t, filepath.Join(networkDir, firewalldb.DBFilename)) +} + +// TestKVDBToSQLProgrammaticMigrationRunsWithOneBBoltDBFiles verifies that the +// migration still runs when only the accounts kvdb exists, creates the missing +// legacy files, and migrates the existing account data to SQL. +func TestKVDBToSQLProgrammaticMigrationRunsWithOneBBoltDBFiles(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testClock := clock.NewDefaultClock() + accountsDir := t.TempDir() + networkDir := t.TempDir() + + accountStore, err := accounts.NewBoltStore( + accountsDir, accounts.DBFilename, testClock, + ) + require.NoError(t, err) + + _, err = accountStore.NewAccount(ctx, 1234, time.Time{}, "acct-1") + require.NoError(t, err) + require.NoError(t, accountStore.Close()) + + sqlStore := sqldb.NewTestSqliteDB(t, db.MakeTestMigrationSets()) + queries := sqlcmig6.NewForType( + sqlStore.BaseDB, sqlStore.BackendType, + ) + + lndClient := newTestLightningClient(t) + + err = kvdbToSqlProgrammaticMigration( + ctx, lndClient, accountsDir, networkDir, + sqlStore.BaseDB, testClock, queries, + ) + require.NoError(t, err) + + requireFileExists(t, filepath.Join(accountsDir, accounts.DBFilename)) + requireFileExists(t, filepath.Join(networkDir, session.DBFilename)) + requireFileExists(t, filepath.Join(networkDir, firewalldb.DBFilename)) + + dbAccounts, err := queries.ListAllAccounts(ctx) + require.NoError(t, err) + require.Len(t, dbAccounts, 1) + + dbSessions, err := queries.ListSessions(ctx) + require.NoError(t, err) + require.Empty(t, dbSessions) +} + +func requireNoFile(t *testing.T, path string) { + t.Helper() + + _, err := os.Stat(path) + require.ErrorIs(t, err, os.ErrNotExist) +} + +func requireFileExists(t *testing.T, path string) { + t.Helper() + + _, err := os.Stat(path) + require.NoError(t, err) +} + +func newTestLightningClient(t *testing.T) lnrpc.LightningClient { + t.Helper() + + lis := bufconn.Listen(1024 * 1024) + server := grpc.NewServer() + lnrpc.RegisterLightningServer(server, &testLightningServer{}) + + go func() { + _ = server.Serve(lis) + }() + + t.Cleanup(func() { + server.Stop() + require.NoError(t, lis.Close()) + }) + + conn, err := grpc.DialContext( + context.Background(), "passthrough:///bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer( + func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }, + ), + ) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, conn.Close()) + }) + + return lnrpc.NewLightningClient(conn) +} + +type testLightningServer struct { + lnrpc.UnimplementedLightningServer +} + +func (t *testLightningServer) ListMacaroonIDs(context.Context, + *lnrpc.ListMacaroonIDsRequest) (*lnrpc.ListMacaroonIDsResponse, error) { + + return &lnrpc.ListMacaroonIDsResponse{}, nil +}