2026-01-21 19:02:03 +01:00
|
|
|
//go:build itest
|
|
|
|
|
|
|
|
|
|
package itest
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-22 14:12:27 +02:00
|
|
|
"bytes"
|
2026-01-21 19:02:03 +01:00
|
|
|
"context"
|
2026-04-02 00:59:42 +02:00
|
|
|
"database/sql"
|
2026-01-21 19:02:03 +01:00
|
|
|
"fmt"
|
2026-04-02 00:59:42 +02:00
|
|
|
"os"
|
2026-01-21 19:02:03 +01:00
|
|
|
"path/filepath"
|
2026-04-02 00:59:42 +02:00
|
|
|
"strings"
|
2026-01-21 19:02:03 +01:00
|
|
|
"time"
|
|
|
|
|
|
2026-04-02 00:59:42 +02:00
|
|
|
_ "github.com/lib/pq"
|
2026-01-21 19:02:03 +01:00
|
|
|
terminal "github.com/lightninglabs/lightning-terminal"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/accounts"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/db/sqlc"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/db/sqlcmig6"
|
2026-04-02 00:59:42 +02:00
|
|
|
"github.com/lightninglabs/lightning-terminal/db/tombstone"
|
2026-01-21 19:02:03 +01:00
|
|
|
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/litrpc"
|
2026-04-02 00:59:42 +02:00
|
|
|
"github.com/lightninglabs/lightning-terminal/session"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/subservers"
|
2026-01-21 19:02:03 +01:00
|
|
|
"github.com/lightningnetwork/lnd/clock"
|
2026-04-02 00:59:42 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
2026-01-21 19:02:03 +01:00
|
|
|
"github.com/lightningnetwork/lnd/sqldb/v2"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// testKvdbSQLMigration implements an itest kvdb -> SQL migration itest. The
|
|
|
|
|
// intention of the itest is to verify that the kvdb -> SQL migration flow works
|
|
|
|
|
// for a litd node which has data inserted to the bbolt database.
|
|
|
|
|
// To verify that, the minimum amount of data is inserted into the bbolt
|
|
|
|
|
// database, which is one object to each database file. Those objects are:
|
|
|
|
|
// * One account - accounts.DB file
|
|
|
|
|
// * One session - session.db file
|
|
|
|
|
// * One action - rules.db file
|
|
|
|
|
//
|
|
|
|
|
// As the respective unit tests for each migration extensively tests that data
|
|
|
|
|
// of all forms for each database file can successfully be migrated, the itest
|
|
|
|
|
// only focuses on testing that the full migration flow works, and leaves the
|
|
|
|
|
// responsibility of data variation coverage to the respective unit test.
|
|
|
|
|
//
|
|
|
|
|
// The test is executed in different steps, and uses RPCs to seed and validate
|
|
|
|
|
// migration fixtures:
|
2026-04-02 00:59:42 +02:00
|
|
|
// 1. Start a node with a bbolt backend.
|
|
|
|
|
// 2. Insert one account, one session and one action via RPC.
|
|
|
|
|
// 3. Snapshot the inserted objects via RPC.
|
|
|
|
|
// 4. Restart with the configured SQL backend to trigger migration.
|
|
|
|
|
// 5. Query objects again via RPC.
|
|
|
|
|
// 6. Compare the new objects to the pre-migration snapshot.
|
|
|
|
|
// 7. Assert the migrated objects in SQL via direct queries, to verify that it's
|
|
|
|
|
// actually the SQL database that contains the migrated objects.
|
|
|
|
|
// 8. Assert that restarting with `databasebackend=bbolt` now fails on
|
|
|
|
|
// `accounts.db`, the first deprecated kvdb file opened during startup.
|
|
|
|
|
// 9. Delete the SQL database and show that SQL startup reruns the migration.
|
|
|
|
|
// 10. Delete `accounts.db` and verify that bbolt startup is still blocked by
|
|
|
|
|
// the next deprecated kvdb file, `session.db`.
|
|
|
|
|
// 11. Delete `session.db` and verify that bbolt startup is still blocked by
|
|
|
|
|
// the next deprecated kvdb file, `rules.db`.
|
2026-01-21 19:02:03 +01:00
|
|
|
func testKvdbSQLMigration(ctx context.Context, net *NetworkHarness,
|
|
|
|
|
t *harnessTest) {
|
|
|
|
|
|
|
|
|
|
if *litDBBackend == terminal.DatabaseBackendBbolt {
|
|
|
|
|
t.t.Skipf("Skipping kvdb migration test for bbolt backend")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:59:42 +02:00
|
|
|
newStepCtx := func() (context.Context, context.CancelFunc) {
|
|
|
|
|
return context.WithTimeout(ctx, defaultTimeout)
|
|
|
|
|
}
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// Step 1: Start a node with a bbolt backend.
|
|
|
|
|
// We want to start from an explicit bbolt backend regardless of the
|
|
|
|
|
// command line flag used to run the itests.
|
|
|
|
|
migNode, err := net.NewNode(
|
|
|
|
|
t.t, "Migrator", nil, false, true,
|
|
|
|
|
fmt.Sprintf(
|
|
|
|
|
"--databasebackend=%s", terminal.DatabaseBackendBbolt,
|
|
|
|
|
),
|
|
|
|
|
"--firewall.request-logger.level=all",
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
defer shutdownAndAssert(net, t, migNode)
|
|
|
|
|
|
|
|
|
|
// Setup a raw gRPC connection used to set up RPC clients.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxt, cancel := newStepCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
rawConn, err := connectRPC(
|
|
|
|
|
ctxt, migNode.Cfg.LitAddr(), migNode.Cfg.LitTLSCertPath,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// Get the litd admin macaroon context from the migration node.
|
|
|
|
|
macBytes := getLiTMacFromFile(t.t, migNode.Cfg)
|
2026-04-02 00:59:42 +02:00
|
|
|
newAdminCtx := func() (context.Context, context.CancelFunc) {
|
|
|
|
|
stepCtx, cancel := newStepCtx()
|
|
|
|
|
return macaroonContext(stepCtx, macBytes), cancel
|
|
|
|
|
}
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// LiT RPC clients are used to seed and assert migration fixtures.
|
|
|
|
|
accountsClient := litrpc.NewAccountsClient(rawConn)
|
|
|
|
|
sessionsClient := litrpc.NewSessionsClient(rawConn)
|
|
|
|
|
autopilotClient := litrpc.NewAutopilotClient(rawConn)
|
|
|
|
|
firewallClient := litrpc.NewFirewallClient(rawConn)
|
|
|
|
|
|
|
|
|
|
// Step 2: Insert one account, one session and one action via RPC.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxm, cancel := newAdminCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
migrationRefs := setupMigrationData(
|
|
|
|
|
ctxm, t, accountsClient, sessionsClient, autopilotClient,
|
|
|
|
|
firewallClient,
|
|
|
|
|
)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// Step 3: Query and snapshot inserted data via RPC while on bbolt.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxm, cancel = newAdminCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
beforeMigration := queryMigrationData(
|
|
|
|
|
ctxm, t, accountsClient, sessionsClient, firewallClient,
|
|
|
|
|
migrationRefs.actionMethod,
|
|
|
|
|
)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// Close now so restarts can reopen stores without locks.
|
|
|
|
|
rawConn.Close()
|
|
|
|
|
|
|
|
|
|
// Step 4: Restart with configured backend to trigger migration.
|
2026-05-22 14:12:27 +02:00
|
|
|
//
|
|
|
|
|
// During the startup, the user will be prompted to confirm the
|
|
|
|
|
// migration by typing yes. We therefore buffer yes to std-in.
|
|
|
|
|
migNode.stdin = bytes.NewBufferString("yes\n")
|
|
|
|
|
|
2026-01-21 19:02:03 +01:00
|
|
|
err = net.RestartNode(
|
|
|
|
|
migNode, func() error { return nil }, []LitArgOption{
|
|
|
|
|
WithLitArg("databasebackend", *litDBBackend),
|
|
|
|
|
WithLitArg("firewall.request-logger.level", "all"),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
// Setup clients for the restarted node once more.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxt, cancel = newStepCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
rawConn, err = connectRPC(
|
|
|
|
|
ctxt, migNode.Cfg.LitAddr(), migNode.Cfg.LitTLSCertPath,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
accountsClient = litrpc.NewAccountsClient(rawConn)
|
|
|
|
|
sessionsClient = litrpc.NewSessionsClient(rawConn)
|
|
|
|
|
firewallClient = litrpc.NewFirewallClient(rawConn)
|
|
|
|
|
|
|
|
|
|
// Step 5: Query migrated data via RPC and compare with pre-migration
|
|
|
|
|
// snapshot.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxm, cancel = newAdminCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
afterMigration := queryMigrationData(
|
|
|
|
|
ctxm, t, accountsClient, sessionsClient, firewallClient,
|
|
|
|
|
migrationRefs.actionMethod,
|
|
|
|
|
)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
2026-01-21 19:02:03 +01:00
|
|
|
|
|
|
|
|
// Step 6: Ensure that the results received by RPC prior and after the
|
|
|
|
|
// migration are equal.
|
|
|
|
|
assertMigrationSnapshotsEqual(t, beforeMigration, afterMigration)
|
|
|
|
|
|
|
|
|
|
// Step 7: Assert migrated data in SQL.
|
2026-04-02 00:59:42 +02:00
|
|
|
ctxt, cancel = newStepCtx()
|
2026-01-21 19:02:03 +01:00
|
|
|
assertMinimalMigrationDataSQL(ctxt, t, migNode, migrationRefs)
|
2026-04-02 00:59:42 +02:00
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
// Step 8: Verify that deprecated kvdb files now block bbolt startup.
|
|
|
|
|
require.NoError(t.t, migNode.Stop())
|
|
|
|
|
|
|
|
|
|
kvdbFiles := migrationKVDBFiles(migNode)
|
|
|
|
|
|
|
|
|
|
assertNodeStartFails(
|
|
|
|
|
t, net, migNode,
|
|
|
|
|
[]LitArgOption{
|
|
|
|
|
WithLitArg(
|
|
|
|
|
"databasebackend",
|
|
|
|
|
terminal.DatabaseBackendBbolt,
|
|
|
|
|
),
|
|
|
|
|
WithLitArg("firewall.request-logger.level", "all"),
|
|
|
|
|
},
|
|
|
|
|
fmt.Sprintf(
|
|
|
|
|
"%v: %v", tombstone.ErrKVDBDeprecated,
|
|
|
|
|
kvdbFiles[0].base,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Step 9: Delete the SQL database and verify that starting with the
|
|
|
|
|
// selected SQL backend reruns the kvdb -> SQL migration successfully.
|
2026-05-22 14:12:27 +02:00
|
|
|
//
|
|
|
|
|
// Note that we do not buffer yes to std-in again, as the prompt will
|
|
|
|
|
// not be shown when the bbolt db has already been tombstoned.
|
2026-04-02 00:59:42 +02:00
|
|
|
rerunSQLMigrationAndAssert(
|
|
|
|
|
t, net, migNode, newStepCtx, newAdminCtx, beforeMigration,
|
|
|
|
|
migrationRefs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Step 10: Delete the first-startup kvdb file and verify that bbolt
|
|
|
|
|
// startup is still blocked by the next deprecated file in the open
|
|
|
|
|
// order.
|
|
|
|
|
removeMigrationKVDBFile(t, kvdbFiles[0])
|
|
|
|
|
assertNodeStartFails(
|
|
|
|
|
t, net, migNode,
|
|
|
|
|
[]LitArgOption{
|
|
|
|
|
WithLitArg(
|
|
|
|
|
"databasebackend",
|
|
|
|
|
terminal.DatabaseBackendBbolt,
|
|
|
|
|
),
|
|
|
|
|
WithLitArg("firewall.request-logger.level", "all"),
|
|
|
|
|
},
|
|
|
|
|
fmt.Sprintf(
|
|
|
|
|
"%v: %v", tombstone.ErrKVDBDeprecated,
|
|
|
|
|
kvdbFiles[1].base,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Step 11: Delete the second-startup kvdb file and verify that bbolt
|
|
|
|
|
// startup is still blocked by the third deprecated file.
|
|
|
|
|
removeMigrationKVDBFile(t, kvdbFiles[1])
|
|
|
|
|
assertNodeStartFails(
|
|
|
|
|
t, net, migNode,
|
|
|
|
|
[]LitArgOption{
|
|
|
|
|
WithLitArg(
|
|
|
|
|
"databasebackend",
|
|
|
|
|
terminal.DatabaseBackendBbolt,
|
|
|
|
|
),
|
|
|
|
|
WithLitArg("firewall.request-logger.level", "all"),
|
|
|
|
|
},
|
|
|
|
|
fmt.Sprintf(
|
|
|
|
|
"%v: %v", tombstone.ErrKVDBDeprecated,
|
|
|
|
|
kvdbFiles[2].base,
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-01-21 19:02:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migrationDataRefs stores stable identifiers used to refetch and assert the
|
|
|
|
|
// test fixtures before and after migration.
|
|
|
|
|
type migrationDataRefs struct {
|
|
|
|
|
accountID string
|
|
|
|
|
sessionID []byte
|
|
|
|
|
actionMethod string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migrationDataSnapshot captures the RPC objects for the migration fixtures at
|
|
|
|
|
// one point in time, so pre/post-migration data can be compared directly.
|
|
|
|
|
type migrationDataSnapshot struct {
|
|
|
|
|
account *litrpc.Account
|
|
|
|
|
session *litrpc.Session
|
|
|
|
|
action *litrpc.Action
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setupMigrationData creates one account, one session and one
|
|
|
|
|
// action through RPCs only. Note that the function intentionally inserts a
|
|
|
|
|
// single object into every kvdb database file, which is the minimal dataset
|
|
|
|
|
// required to verify that data from each database file gets migrated to SQL
|
|
|
|
|
// when executing the itest.
|
|
|
|
|
func setupMigrationData(adminCtx context.Context,
|
|
|
|
|
t *harnessTest,
|
|
|
|
|
accountsClient litrpc.AccountsClient,
|
|
|
|
|
sessionsClient litrpc.SessionsClient,
|
|
|
|
|
autopilotClient litrpc.AutopilotClient,
|
|
|
|
|
firewallClient litrpc.FirewallClient) migrationDataRefs {
|
|
|
|
|
|
|
|
|
|
// 1. Insert an account.
|
|
|
|
|
accountResp, err := accountsClient.CreateAccount(
|
|
|
|
|
adminCtx, &litrpc.CreateAccountRequest{
|
|
|
|
|
AccountBalance: 10_000,
|
|
|
|
|
ExpirationDate: time.Now().Add(time.Hour).Unix(),
|
|
|
|
|
Label: "migration-rpc-account",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
// 2. Insert a session.
|
|
|
|
|
sessionResp, err := sessionsClient.AddSession(
|
|
|
|
|
adminCtx, &litrpc.AddSessionRequest{
|
|
|
|
|
Label: "migration-rpc-session",
|
|
|
|
|
SessionType: litrpc.SessionType_TYPE_MACAROON_ADMIN,
|
|
|
|
|
ExpiryTimestampSeconds: uint64(
|
|
|
|
|
time.Now().Add(30 * time.Minute).Unix(),
|
|
|
|
|
),
|
|
|
|
|
MailboxServerAddr: mailboxServerAddr,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
// 3. Insert an action.
|
|
|
|
|
_, err = autopilotClient.ListAutopilotFeatures(
|
|
|
|
|
adminCtx, &litrpc.ListAutopilotFeaturesRequest{},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
// Note that this function intentionally uses the ListAutopilotFeatures
|
|
|
|
|
// method, as that then becomes easily queryable. As the
|
|
|
|
|
// firewall.request-logger.level=all config option is used for this
|
|
|
|
|
// test, more than one action will be created during its execution.
|
|
|
|
|
// Therefore, using this specific method makes it easy to query this
|
|
|
|
|
// specific action added below.
|
|
|
|
|
actionMethod := "/litrpc.Autopilot/ListAutopilotFeatures"
|
|
|
|
|
actionsResp, err := firewallClient.ListActions(
|
|
|
|
|
adminCtx, &litrpc.ListActionsRequest{
|
|
|
|
|
MethodName: actionMethod,
|
|
|
|
|
StartTimestamp: uint64(time.Now().Unix()),
|
|
|
|
|
MaxNumActions: 1,
|
|
|
|
|
Reversed: true,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.Len(t.t, actionsResp.Actions, 1)
|
|
|
|
|
|
|
|
|
|
return migrationDataRefs{
|
|
|
|
|
accountID: accountResp.Account.Id,
|
|
|
|
|
sessionID: sessionResp.Session.Id,
|
|
|
|
|
actionMethod: actionMethod,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// queryMigrationData fetches the migration fixtures via RPC.
|
|
|
|
|
func queryMigrationData(adminCtx context.Context, t *harnessTest,
|
|
|
|
|
accountsClient litrpc.AccountsClient,
|
|
|
|
|
sessionsClient litrpc.SessionsClient,
|
|
|
|
|
firewallClient litrpc.FirewallClient,
|
|
|
|
|
actionMethod string) migrationDataSnapshot {
|
|
|
|
|
|
|
|
|
|
accountsResp, err := accountsClient.ListAccounts(
|
|
|
|
|
adminCtx, &litrpc.ListAccountsRequest{},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.Len(t.t, accountsResp.Accounts, 1)
|
|
|
|
|
|
|
|
|
|
sessionsResp, err := sessionsClient.ListSessions(
|
|
|
|
|
adminCtx, &litrpc.ListSessionsRequest{},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.Len(t.t, sessionsResp.Sessions, 1)
|
|
|
|
|
|
|
|
|
|
actionsResp, err := firewallClient.ListActions(
|
|
|
|
|
adminCtx, &litrpc.ListActionsRequest{
|
|
|
|
|
MethodName: actionMethod,
|
|
|
|
|
Reversed: true,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.Len(t.t, actionsResp.Actions, 1)
|
|
|
|
|
|
|
|
|
|
return migrationDataSnapshot{
|
|
|
|
|
account: accountsResp.Accounts[0],
|
|
|
|
|
session: sessionsResp.Sessions[0],
|
|
|
|
|
action: actionsResp.Actions[0],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertMigrationSnapshotsEqual asserts that the post-migration RPC
|
|
|
|
|
// snapshot matches the pre-migration RPC snapshot. The session comparison
|
|
|
|
|
// applies a focused normalization for known kvdb vs SQL macaroon recipe
|
|
|
|
|
// representation differences.
|
|
|
|
|
func assertMigrationSnapshotsEqual(t *harnessTest,
|
|
|
|
|
before migrationDataSnapshot,
|
|
|
|
|
after migrationDataSnapshot) {
|
|
|
|
|
|
|
|
|
|
require.NotNil(t.t, before.account)
|
|
|
|
|
require.NotNil(t.t, before.session)
|
|
|
|
|
require.NotNil(t.t, before.action)
|
|
|
|
|
require.NotNil(t.t, after.account)
|
|
|
|
|
require.NotNil(t.t, after.session)
|
|
|
|
|
require.NotNil(t.t, after.action)
|
|
|
|
|
|
|
|
|
|
// As a nil macaroon recipe will be represented differently in kvdb vs
|
|
|
|
|
// SQL dbs, we need to override that field in the session.
|
|
|
|
|
overrideRPCMacaroonRecipe(before.session, after.session)
|
|
|
|
|
|
|
|
|
|
require.Equal(t.t, before.account, after.account)
|
|
|
|
|
require.Equal(t.t, before.session, after.session)
|
|
|
|
|
require.Equal(t.t, before.action, after.action)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// overrideRPCMacaroonRecipe normalizes the one known RPC representation
|
|
|
|
|
// mismatch in this test: kvdb can return a non-nil MacaroonRecipe while SQL can
|
|
|
|
|
// return nil for equivalent empty recipe data.
|
|
|
|
|
func overrideRPCMacaroonRecipe(kvSession *litrpc.Session,
|
|
|
|
|
sqlSession *litrpc.Session) {
|
|
|
|
|
|
|
|
|
|
if kvSession == nil || sqlSession == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize SQL nil vs empty recipe when kvdb had an explicit empty
|
|
|
|
|
// macaroon recipe.
|
|
|
|
|
if kvSession.MacaroonRecipe != nil && sqlSession.MacaroonRecipe == nil {
|
|
|
|
|
sqlSession.MacaroonRecipe = &litrpc.MacaroonRecipe{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertMinimalMigrationDataSQL checks that migration fixtures are available
|
|
|
|
|
// through SQL-backed stores after migration. Note that the SQL assertion is
|
|
|
|
|
// only intended to verify that an entry for the respective inserted object
|
|
|
|
|
// exists in the database, and not that all fields are the same. Test coverage
|
|
|
|
|
// for that has already been implemented in unit tests, which is responsible for
|
|
|
|
|
// those assertions.
|
|
|
|
|
func assertMinimalMigrationDataSQL(ctx context.Context, t *harnessTest,
|
|
|
|
|
node *HarnessNode, data migrationDataRefs) {
|
|
|
|
|
|
|
|
|
|
sqlStore := openMigrationSQLStore(t, node)
|
|
|
|
|
defer sqlStore.Close()
|
|
|
|
|
|
|
|
|
|
queries := sqlcmig6.NewForType(sqlStore, sqlStore.BackendType)
|
|
|
|
|
|
|
|
|
|
accountsList, err := queries.ListAllAccounts(ctx)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
require.Len(t.t, accountsList, 1)
|
|
|
|
|
dbAccount := accountsList[0]
|
|
|
|
|
|
|
|
|
|
alias, err := accounts.AccountIDFromInt64(dbAccount.Alias)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
require.Equal(t.t, data.accountID, alias.String())
|
|
|
|
|
|
|
|
|
|
sessions, err := queries.ListSessions(ctx)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.Len(t.t, sessions, 1)
|
|
|
|
|
|
|
|
|
|
dbSession := sessions[0]
|
|
|
|
|
require.Equal(t.t, data.sessionID, dbSession.Alias)
|
|
|
|
|
|
|
|
|
|
actionStore := firewalldb.NewSQLDB(
|
|
|
|
|
sqlStore, sqlc.NewForType(sqlStore, sqlStore.BackendType),
|
|
|
|
|
clock.NewDefaultClock(),
|
|
|
|
|
)
|
|
|
|
|
actions, _, _, err := actionStore.ListActions(
|
|
|
|
|
ctx, nil, firewalldb.WithActionMethodName(data.actionMethod),
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
require.NotEmpty(t.t, actions)
|
|
|
|
|
require.Len(t.t, actions, 1)
|
|
|
|
|
|
|
|
|
|
require.Equal(t.t, data.actionMethod, actions[0].RPCMethod)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:59:42 +02:00
|
|
|
// rerunSQLMigrationAndAssert removes the SQL database, starts litd with the
|
|
|
|
|
// selected SQL backend to rerun the kvdb migration, then verifies the fixtures
|
|
|
|
|
// via RPC and direct SQL queries.
|
|
|
|
|
func rerunSQLMigrationAndAssert(t *harnessTest, net *NetworkHarness,
|
|
|
|
|
node *HarnessNode,
|
|
|
|
|
newStepCtx func() (context.Context, context.CancelFunc),
|
|
|
|
|
newAdminCtx func() (context.Context, context.CancelFunc),
|
|
|
|
|
beforeMigration migrationDataSnapshot, refs migrationDataRefs) {
|
|
|
|
|
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
deleteMigrationSQLDB(t, node)
|
|
|
|
|
|
|
|
|
|
err := node.Start(
|
|
|
|
|
net.litdBinary, net.lndErrorChan, true,
|
|
|
|
|
WithLitArg("databasebackend", *litDBBackend),
|
|
|
|
|
WithLitArg("firewall.request-logger.level", "all"),
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
ctxt, cancel := newStepCtx()
|
|
|
|
|
rawConn, err := connectRPC(
|
|
|
|
|
ctxt, node.Cfg.LitAddr(), node.Cfg.LitTLSCertPath,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
cancel()
|
|
|
|
|
defer rawConn.Close()
|
|
|
|
|
|
|
|
|
|
accountsClient := litrpc.NewAccountsClient(rawConn)
|
|
|
|
|
sessionsClient := litrpc.NewSessionsClient(rawConn)
|
|
|
|
|
firewallClient := litrpc.NewFirewallClient(rawConn)
|
|
|
|
|
|
|
|
|
|
ctxm, cancel := newAdminCtx()
|
|
|
|
|
afterMigration := queryMigrationData(
|
|
|
|
|
ctxm, t, accountsClient, sessionsClient, firewallClient,
|
|
|
|
|
refs.actionMethod,
|
|
|
|
|
)
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
assertMigrationSnapshotsEqual(t, beforeMigration, afterMigration)
|
|
|
|
|
|
|
|
|
|
ctxt, cancel = newStepCtx()
|
|
|
|
|
assertMinimalMigrationDataSQL(ctxt, t, node, refs)
|
|
|
|
|
cancel()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type migrationKVDBFile struct {
|
|
|
|
|
base string
|
|
|
|
|
path string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migrationKVDBFiles returns the startup order of the deprecated kvdb files
|
|
|
|
|
// when litd opens the bbolt backend: accounts, then sessions, then firewall.
|
|
|
|
|
func migrationKVDBFiles(node *HarnessNode) []migrationKVDBFile {
|
|
|
|
|
networkDir := filepath.Join(node.Cfg.LitDir, node.Cfg.NetParams.Name)
|
|
|
|
|
|
|
|
|
|
return []migrationKVDBFile{
|
|
|
|
|
{
|
|
|
|
|
base: accounts.DBFilename,
|
|
|
|
|
path: filepath.Join(networkDir, accounts.DBFilename),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
base: session.DBFilename,
|
|
|
|
|
path: filepath.Join(networkDir, session.DBFilename),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
base: firewalldb.DBFilename,
|
|
|
|
|
path: filepath.Join(networkDir, firewalldb.DBFilename),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// removeMigrationKVDBFile deletes one kvdb file so the next deprecated file in
|
|
|
|
|
// the bbolt startup order becomes the first blocker.
|
|
|
|
|
func removeMigrationKVDBFile(t *harnessTest, file migrationKVDBFile) {
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
err := os.Remove(file.path)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:02:03 +01:00
|
|
|
// openMigrationSQLStore opens a SQL database handle for the backend selected
|
|
|
|
|
// by the itest `-litdbbackend` flag.
|
|
|
|
|
//
|
|
|
|
|
// The migration test starts on bbolt and then restarts litd with
|
|
|
|
|
// `-litdbbackend=<backend>` to trigger kvdb -> SQL migration. This helper
|
|
|
|
|
// mirrors that backend choice and opens the matching SQL store so the test can
|
|
|
|
|
// assert migrated rows directly:
|
|
|
|
|
// - `sqlite`: opens the node-local `litd.db` SQLite file.
|
|
|
|
|
// - `postgres`: opens the configured Postgres database from the node config.
|
|
|
|
|
//
|
|
|
|
|
// The returned value is always a `*sqldb.BaseDB`, independent of which SQL
|
|
|
|
|
// implementation is used underneath.
|
|
|
|
|
func openMigrationSQLStore(t *harnessTest,
|
|
|
|
|
node *HarnessNode) *sqldb.BaseDB {
|
|
|
|
|
|
|
|
|
|
switch *litDBBackend {
|
|
|
|
|
case terminal.DatabaseBackendPostgres:
|
|
|
|
|
pgConf := node.Cfg.PostgresConfig
|
|
|
|
|
|
|
|
|
|
require.NotNil(
|
|
|
|
|
t.t, pgConf,
|
|
|
|
|
"postgres config required for postgres backend",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
sqlStore, err := sqldb.NewPostgresStore(
|
|
|
|
|
&sqldb.PostgresConfig{
|
|
|
|
|
Dsn: pgConf.DSN(false),
|
|
|
|
|
SkipMigrations: true,
|
|
|
|
|
RequireSSL: pgConf.RequireSSL,
|
|
|
|
|
MaxOpenConnections: pgConf.MaxOpenConnections,
|
|
|
|
|
MaxIdleConnections: pgConf.MaxIdleConnections,
|
|
|
|
|
ConnMaxLifetime: pgConf.ConnMaxLifetime,
|
|
|
|
|
ConnMaxIdleTime: pgConf.ConnMaxIdleTime,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
return sqlStore.BaseDB
|
|
|
|
|
|
|
|
|
|
case terminal.DatabaseBackendSqlite:
|
|
|
|
|
dbPath := filepath.Join(
|
|
|
|
|
node.Cfg.LitDir, node.Cfg.NetParams.Name, "litd.db",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
sqlStore, err := sqldb.NewSqliteStore(
|
|
|
|
|
&sqldb.SqliteConfig{
|
|
|
|
|
SkipMigrations: true,
|
|
|
|
|
SkipMigrationDbBackup: true,
|
|
|
|
|
}, dbPath,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
return sqlStore.BaseDB
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
t.t.Fatalf("unsupported sql backend %v", *litDBBackend)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-02 00:59:42 +02:00
|
|
|
|
|
|
|
|
// assertNodeStartFails waits for the node startup path to complete and asserts
|
|
|
|
|
// that startup fails with an error containing the expected text.
|
|
|
|
|
func assertNodeStartFails(t *harnessTest, net *NetworkHarness,
|
|
|
|
|
node *HarnessNode, litArgOpts []LitArgOption, expectedErr string) {
|
|
|
|
|
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
if expectedErr != "" {
|
|
|
|
|
err := node.Start(
|
|
|
|
|
net.litdBinary, net.lndErrorChan,
|
|
|
|
|
false, litArgOpts...,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
assertLitStatusError(t, node, expectedErr)
|
|
|
|
|
|
|
|
|
|
if node.cmd != nil && node.cmd.Process != nil {
|
|
|
|
|
_ = node.cmd.Process.Kill()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
waitForNodeExit(t, node)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := node.Start(
|
|
|
|
|
net.litdBinary, net.lndErrorChan, true,
|
|
|
|
|
litArgOpts...,
|
|
|
|
|
)
|
|
|
|
|
require.Error(t.t, err)
|
|
|
|
|
|
|
|
|
|
waitForNodeExit(t, node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertLitStatusError waits for the public LiT status endpoint to report the
|
|
|
|
|
// expected error string for the LiT sub-server.
|
|
|
|
|
func assertLitStatusError(t *harnessTest, node *HarnessNode,
|
|
|
|
|
expectedErr string) {
|
|
|
|
|
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
err := wait.NoError(func() error {
|
|
|
|
|
ctxt, cancel := context.WithTimeout(
|
|
|
|
|
context.Background(), 2*time.Second,
|
|
|
|
|
)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
rawConn, err := connectLitRPC(
|
|
|
|
|
ctxt, node.Cfg.LitAddr(), node.Cfg.LitTLSCertPath, "",
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer rawConn.Close()
|
|
|
|
|
|
|
|
|
|
statusClient := litrpc.NewStatusClient(rawConn)
|
|
|
|
|
ctxt, cancel = context.WithTimeout(
|
|
|
|
|
context.Background(), 2*time.Second,
|
|
|
|
|
)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
resp, err := statusClient.SubServerStatus(
|
|
|
|
|
ctxt, &litrpc.SubServerStatusReq{},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
litStatus, ok := resp.SubServers[subservers.LIT]
|
|
|
|
|
if ok && strings.Contains(litStatus.GetError(), expectedErr) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"lit status error %q did not contain %q",
|
|
|
|
|
litStatus.GetError(), expectedErr,
|
|
|
|
|
)
|
|
|
|
|
}, defaultTimeout)
|
|
|
|
|
|
|
|
|
|
require.NoErrorf(
|
|
|
|
|
t.t, err, "expected %s status failure but got:", expectedErr,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// waitForNodeExit waits for the node process to exit and force kills it if the
|
|
|
|
|
// failed-start path leaves the process around.
|
|
|
|
|
func waitForNodeExit(t *harnessTest, node *HarnessNode) {
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-node.processExit:
|
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
|
if node.cmd != nil && node.cmd.Process != nil {
|
|
|
|
|
_ = node.cmd.Process.Kill()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-node.processExit:
|
|
|
|
|
case <-time.After(10 * time.Second):
|
|
|
|
|
t.t.Fatalf("timed out waiting for %s process exit",
|
|
|
|
|
node.Name())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// deleteMigrationSQLDB removes the SQL database contents so starting LiT with
|
|
|
|
|
// the SQL backend reruns the kvdb -> SQL migration.
|
|
|
|
|
func deleteMigrationSQLDB(t *harnessTest, node *HarnessNode) {
|
|
|
|
|
t.t.Helper()
|
|
|
|
|
|
|
|
|
|
switch *litDBBackend {
|
|
|
|
|
case terminal.DatabaseBackendSqlite:
|
|
|
|
|
dbPath := filepath.Join(
|
|
|
|
|
node.Cfg.LitDir, node.Cfg.NetParams.Name, "litd.db",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
err := os.Remove(dbPath)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
case terminal.DatabaseBackendPostgres:
|
|
|
|
|
pgConf := node.Cfg.PostgresConfig
|
|
|
|
|
require.NotNil(t.t, pgConf)
|
|
|
|
|
|
|
|
|
|
dbConn, err := sql.Open("postgres", pgConf.DSN(false))
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
defer dbConn.Close()
|
|
|
|
|
|
|
|
|
|
_, err = dbConn.ExecContext(
|
|
|
|
|
context.Background(),
|
|
|
|
|
`DROP SCHEMA IF EXISTS public CASCADE;
|
|
|
|
|
CREATE SCHEMA public;`,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t.t, err)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
t.t.Fatalf("unsupported sql backend %v", *litDBBackend)
|
|
|
|
|
}
|
|
|
|
|
}
|