lightning-terminal/firewalldb/kvdb_store.go
Viktor Torstensson 9016bbb3d2
multi: confirm kvdb migration at startup
Prompt before automatically migrating legacy kvdb state to SQL when
litd starts with a SQL backend and active bbolt data is still present.

Detect prior migrations by checking for the SQL tombstone marker so
already-migrated stores can start without prompting. Add unit coverage
for the prompt flow and wire stdin through the itest harness so the
migration restart path can acknowledge the prompt automatically.
2026-06-08 21:11:51 +02:00

237 lines
6 KiB
Go

package firewalldb
import (
"context"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"time"
"github.com/lightninglabs/lightning-terminal/db/tombstone"
"github.com/lightningnetwork/lnd/clock"
"go.etcd.io/bbolt"
)
const (
// DBFilename is the default filename of the rules' database.
DBFilename = "rules.db"
// dbFilePermission is the default permission the rules' database file
// is created with.
dbFilePermission = 0600
// DefaultRulesDBTimeout is the default maximum time we wait for the
// db bbolt database to be opened. If the database is already
// opened by another process, the unique lock cannot be obtained. With
// the timeout we error out after the given time instead of just
// blocking for forever.
DefaultRulesDBTimeout = 5 * time.Second
)
var (
// byteOrder is the default byte order we'll use for serialization
// within the database.
byteOrder = binary.BigEndian
)
// BoltDB is a bolt-backed persistent store.
type BoltDB struct {
*bbolt.DB
clock clock.Clock
sessionIDIndex SessionDB
accountsDB AccountsDB
}
// NewBoltDB creates a new bolt database that can be found at the given
// directory.
func NewBoltDB(dir, fileName string, sessionIDIndex SessionDB,
accountsDB AccountsDB, clock clock.Clock) (*BoltDB, error) {
return newBoltDB(
dir, fileName, sessionIDIndex, accountsDB, clock, false,
)
}
// NewBoltDBForMigration opens the rules kvdb store even if it was already
// marked as deprecated. This is only intended for rerunning the kvdb to SQL
// migration after the SQL database was removed or downgraded.
func NewBoltDBForMigration(dir, fileName string, sessionIDIndex SessionDB,
accountsDB AccountsDB, clock clock.Clock) (*BoltDB, error) {
return newBoltDB(
dir, fileName, sessionIDIndex, accountsDB, clock, true,
)
}
// DeprecateKVDB marks the rules kvdb file in the given db directory as
// deprecated after a successful SQL migration.
func DeprecateKVDB(dbDir string) error {
return tombstone.DeprecateKVDB(
filepath.Join(dbDir, DBFilename), DefaultRulesDBTimeout,
rulesBucketKey,
)
}
// HasActiveKVDB reports whether the rules kvdb file in the given db directory
// still holds data that is pending migration to SQL.
func HasActiveKVDB(dbDir string) (bool, error) {
return tombstone.HasActiveKVDB(
filepath.Join(dbDir, DBFilename), rulesBucketKey,
DefaultRulesDBTimeout,
)
}
func newBoltDB(dir, fileName string, sessionIDIndex SessionDB,
accountsDB AccountsDB, clock clock.Clock,
allowDeprecated bool) (*BoltDB, error) {
firstInit := false
path := filepath.Join(dir, fileName)
// If the database file does not exist yet, create its directory.
if !fileExists(path) {
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, err
}
firstInit = true
}
if !allowDeprecated {
err := tombstone.CheckKVDBDeprecated(
path, rulesBucketKey, DefaultRulesDBTimeout,
)
if err != nil {
return nil, err
}
}
db, err := initDB(path, firstInit)
if err != nil {
return nil, err
}
// Attempt to sync the database's current version with the latest known
// version available.
if !allowDeprecated {
if err := syncVersions(db); err != nil {
return nil, err
}
}
return &BoltDB{
DB: db,
sessionIDIndex: sessionIDIndex,
accountsDB: accountsDB,
clock: clock,
}, nil
}
// fileExists reports whether the named file or directory exists.
func fileExists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// initDB initializes all the required top-level buckets for the database.
func initDB(filepath string, firstInit bool) (*bbolt.DB, error) {
db, err := bbolt.Open(filepath, dbFilePermission, &bbolt.Options{
Timeout: DefaultRulesDBTimeout,
})
if err == bbolt.ErrTimeout {
return nil, fmt.Errorf("error while trying to open %s: timed "+
"out after %v when trying to obtain exclusive lock",
filepath, DefaultRulesDBTimeout)
}
if err != nil {
return nil, err
}
err = db.Update(func(tx *bbolt.Tx) error {
if firstInit {
metadataBucket, err := tx.CreateBucketIfNotExists(
metadataBucketKey,
)
if err != nil {
return err
}
err = setDBVersion(metadataBucket, latestDBVersion)
if err != nil {
return err
}
}
_, err := tx.CreateBucketIfNotExists(rulesBucketKey)
if err != nil {
return err
}
actionsBucket, err := tx.CreateBucketIfNotExists(
actionsBucketKey,
)
if err != nil {
return err
}
_, err = actionsBucket.CreateBucketIfNotExists(actionsKey)
if err != nil {
return err
}
_, err = actionsBucket.CreateBucketIfNotExists(actionsIndex)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(privacyBucketKey)
return err
})
if err != nil {
return nil, err
}
return db, nil
}
// kvdbExecutor is a concrete implementation of the DBExecutor interface that
// uses a bbolt database as its backing store.
type kvdbExecutor[T any] struct {
db *bbolt.DB
wrapTx func(tx *bbolt.Tx) T
}
// Update opens a database read/write transaction and executes the function f
// with the transaction passed as a parameter. After f exits, if f did not
// error, the transaction is committed. Otherwise, if f did error, the
// transaction is rolled back. If the rollback fails, the original error
// returned by f is still returned. If the commit fails, the commit error is
// returned.
//
// NOTE: this is part of the DBExecutor interface.
func (e *kvdbExecutor[T]) Update(ctx context.Context,
fn func(ctx context.Context, tx T) error) error {
return e.db.Update(func(tx *bbolt.Tx) error {
return fn(ctx, e.wrapTx(tx))
})
}
// View opens a database read transaction and executes the function f with the
// transaction passed as a parameter. After f exits, the transaction is rolled
// back. If f errors, its error is returned, not a rollback error (if any
// occur).
//
// NOTE: this is part of the DBExecutor interface.
func (e *kvdbExecutor[T]) View(ctx context.Context,
fn func(ctx context.Context, tx T) error) error {
return e.db.View(func(tx *bbolt.Tx) error {
return fn(ctx, e.wrapTx(tx))
})
}