payments/migration1: wire KV→SQL migration in the main pkg
What changed, and why it matters
This commit wires a new experimental database migration into LND that moves payment data from an older key-value store to a newer SQL-based store. It also adds a 'tombstone' marker so that, once the migration runs, the old key-value store cannot be used again. The change is only active when a special test build tag is used, so it does not affect normal production builds. There is no direct security vulnerability visible in the diff, but any database migration carries operational risks such as data loss or being unable to restart the node if something goes wrong.
Treat this as an infrastructure/operational change rather than a security patch. Reviewers should verify that the migration is correctly gated by the build tag, that tombstone handling cannot be bypassed, and that the migration function handles partial failures safely. Users on experimental 'test_native_sql' builds should back up their databases before upgrading and monitor startup logs for tombstone-related errors.
Security signals we found
New database migration path added for payments data
Tombstone mechanism prevents fallback to KV backend after migration
Migration is gated by experimental build tag 'test_native_sql'
Startup abort if tombstone detected while using KV backend
No input validation or access-control changes visible in diff
Evidence from the diff
The commit hooks the payments KV-to-SQL migration (payments/db/migration1) into the SQL migration framework. It adds a migration entry at version 14 in sqldb/migrations_dev.go, attaches the migration function in config_builder.go, and introduces payments/db/kv_tombstone.go to set and check a tombstone key in the legacy payments bucket. On completion, SetPaymentsBucketTombstone writes a marker; on subsequent KV-backend startup, GetPaymentsBucketTombstone aborts startup with an error telling the operator to switch back to native SQL. The migration remains gated behind the ‘test_native_sql’ build tag, as noted in the commit message.
Changed components
config_builder.gopayments/db/kv_tombstone.gopayments/db/log.gosqldb/migrations_dev.gopayments/db/migration1Inspect captured patch +145 / −0
diff --git a/config_builder.go b/config_builder.go
index 74f8b3a..729cb60 100644
--- a/config_builder.go
+++ b/config_builder.go
@@ -51,6 +51,8 @@ import (
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/msgmux"
paymentsdb "github.com/lightningnetwork/lnd/payments/db"
+ paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
+ paymentsmig1sqlc "github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sqldb"
@@ -76,6 +78,10 @@ const (
// graphMigration is the version number for the graph migration
// that migrates the KV graph to the native SQL schema.
graphMigration = 10
+
+ // paymentMigration is the version number for the payments migration
+ // that migrates KV payments to the native SQL schema.
+ paymentMigration = 14
)
// GrpcRegistrar is an interface that must be satisfied by an external subserver
@@ -1153,6 +1159,31 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
return nil
}
+ paymentMig := func(tx *sqlc.Queries) error {
+ err := paymentsmig1.MigratePaymentsKVToSQL(
+ ctx,
+ dbs.ChanStateDB.Backend,
+ paymentsmig1sqlc.New(tx.GetTx()),
+ &paymentsmig1.SQLStoreConfig{
+ QueryCfg: queryCfg,
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("failed to migrate "+
+ "payments to SQL: %w", err)
+ }
+
+ // Set the payments bucket tombstone to
+ // indicate that the migration has been
+ // completed.
+ d.logger.Debugf("Setting payments bucket " +
+ "tombstone")
+
+ return paymentsdb.SetPaymentsBucketTombstone(
+ dbs.ChanStateDB.Backend,
+ )
+ }
+
// Make sure we attach the custom migration function to
// the correct migration version.
for i := 0; i < len(migrations); i++ {
@@ -1162,11 +1193,17 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
migrations[i].MigrationFn = invoiceMig
continue
+
case graphMigration:
migrations[i].MigrationFn = graphMig
continue
+ case paymentMigration:
+ migrations[i].MigrationFn = paymentMig
+
+ continue
+
default:
}
@@ -1265,6 +1302,27 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
return nil, nil, err
}
+ // Check if the payments bucket tombstone is set. If it is, we
+ // need to return and ask the user switch back to using the
+ // native SQL store.
+ ripPayments, err := paymentsdb.GetPaymentsBucketTombstone(
+ dbs.ChanStateDB.Backend,
+ )
+ if err != nil {
+ err = fmt.Errorf("unable to check payments bucket "+
+ "tombstone: %w", err)
+ d.logger.Error(err)
+
+ return nil, nil, err
+ }
+ if ripPayments {
+ err = fmt.Errorf("payments bucket tombstoned, please " +
+ "switch back to native SQL")
+ d.logger.Error(err)
+
+ return nil, nil, err
+ }
+
dbs.InvoiceDB = dbs.ChanStateDB
graphStore, err = graphdb.NewKVStore(
diff --git a/payments/db/kv_tombstone.go b/payments/db/kv_tombstone.go
new file mode 100644
index 0000000..7dc13b9
--- /dev/null
+++ b/payments/db/kv_tombstone.go
@@ -0,0 +1,71 @@
+package paymentsdb
+
+import (
+ "fmt"
+
+ "github.com/lightningnetwork/lnd/kvdb"
+)
+
+var (
+ // paymentsBucketTombstone is the key used to mark the payments bucket
+ // as permanently closed after a successful migration.
+ paymentsBucketTombstone = []byte("payments-tombstone")
+)
+
+// SetPaymentsBucketTombstone sets the tombstone key in the payments bucket to
+// mark the bucket as permanently closed. This prevents it from being reopened
+// in the future.
+func SetPaymentsBucketTombstone(db kvdb.Backend) error {
+ return kvdb.Update(db, func(tx kvdb.RwTx) error {
+ // Access the top-level payments bucket.
+ payments := tx.ReadWriteBucket(paymentsRootBucket)
+
+ // In case the bucket doesn't exist, because we start
+ // immediately with the native SQL schema, we create it as well
+ // to make sure the user cannot switch back to the KV store.
+ if payments == nil {
+ var err error
+ payments, err = tx.CreateTopLevelBucket(
+ paymentsRootBucket,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to create payments "+
+ "bucket: %w", err)
+ }
+ }
+
+ // Add the tombstone key to the payments bucket.
+ err := payments.Put(paymentsBucketTombstone, []byte("1"))
+ if err != nil {
+ return fmt.Errorf("failed to set tombstone: %w", err)
+ }
+
+ return nil
+ }, func() {})
+}
+
+// GetPaymentsBucketTombstone checks if the tombstone key exists in the payments
+// bucket. It returns true if the tombstone is present and false otherwise.
+func GetPaymentsBucketTombstone(db kvdb.Backend) (bool, error) {
+ var tombstoneExists bool
+
+ err := kvdb.View(db, func(tx kvdb.RTx) error {
+ // Access the top-level payments bucket.
+ payments := tx.ReadBucket(paymentsRootBucket)
+ if payments == nil {
+ tombstoneExists = false
+ return nil
+ }
+
+ // Check if the tombstone key exists.
+ tombstone := payments.Get(paymentsBucketTombstone)
+ tombstoneExists = tombstone != nil
+
+ return nil
+ }, func() {})
+ if err != nil {
+ return false, err
+ }
+
+ return tombstoneExists, nil
+}
diff --git a/payments/db/log.go b/payments/db/log.go
index 8a77dbc..c889234 100644
--- a/payments/db/log.go
+++ b/payments/db/log.go
@@ -3,6 +3,7 @@ package paymentsdb
import (
"github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/build"
+ paymentsmig1 "github.com/lightningnetwork/lnd/payments/db/migration1"
)
// log is a logger that is initialized with no output filters. This
@@ -29,4 +30,5 @@ func DisableLog() {
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
+ paymentsmig1.UseLogger(logger)
}
diff --git a/sqldb/migrations_dev.go b/sqldb/migrations_dev.go
index ca57cf0..7b02b15 100644
--- a/sqldb/migrations_dev.go
+++ b/sqldb/migrations_dev.go
@@ -8,4 +8,18 @@ var migrationAdditions = []MigrationConfig{
Version: 12,
SchemaVersion: 10,
},
+ {
+ Name: "000011_payment_duplicates",
+ Version: 13,
+ SchemaVersion: 11,
+ },
+ {
+ Name: "kv_payments_migration",
+ Version: 14,
+ SchemaVersion: 11,
+ // A migration function may be attached to this
+ // migration to migrate KV payments to the native SQL
+ // schema. This is optional and can be disabled by the
+ // user if necessary.
+ },
}
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.