What changed, and why it matters
This commit removes a separate 'tombstone' marker for payment data migrations and instead relies on a single invoice tombstone to prevent users from switching back to an older database format. It also adds missing cleanup calls when database setup fails and flags a pre-existing crash-safety weakness in how the invoice tombstone is set during SQL migrations. It is a hardening/refactoring change rather than a fix for an active exploit.
Treat as routine hardening. Users running native SQL mode should ensure they are on a version that includes this commit before any future migration tooling changes. No immediate action required for operators.
Security signals we found
Removal of redundant KV tombstone reduces state-consistency attack surface
Added missing cleanUp() calls on early error returns prevents resource leaks
TODO comment documents crash-safety fragility in migration tombstone logic
Evidence from the diff
The patch deletes payments/db/kv_tombstone.go and the calls that set/check a payments-bucket tombstone. The invoice bucket tombstone is now used as the system-wide guard against reverting from native SQL to KV mode. The migration callback now returns nil after migrating payments instead of writing a KV tombstone. Several error paths in BuildDatabase now call cleanUp() before returning. A TODO comment notes that the invoice tombstone is currently written inside the SQL transaction callback, which is fragile if the SQL transaction retries before commit.
Changed components
lnd/config_builder.gopayments/db/kv_tombstone.go (deleted)Inspect captured patch +20 / −101
diff --git a/config_builder.go b/config_builder.go
index ef3eff9..1a18f21 100644
--- a/config_builder.go
+++ b/config_builder.go
@@ -1134,6 +1134,16 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
// Set the invoice bucket tombstone to indicate
// that the migration has been completed.
+ //
+ // TODO(ziggie): The tombstone is currently
+ // set inside the SQL transaction callback,
+ // which is fragile: if the SQL transaction
+ // is retried (e.g. on a serialization
+ // error), the KV tombstone is written before
+ // the SQL commit is confirmed. Move this to
+ // run after ApplyAllMigrations returns so
+ // the tombstone is only set once the
+ // migration is durably committed.
d.logger.Debugf("Setting invoice bucket " +
"tombstone")
@@ -1173,15 +1183,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
"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,
- )
+ return nil
}
// Make sure we attach the custom migration function to
@@ -1260,6 +1262,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
graphExecutor, graphDBOptions...,
)
if err != nil {
+ cleanUp()
err = fmt.Errorf("unable to get graph store: %w", err)
d.logger.Error(err)
@@ -1275,6 +1278,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
baseDB, dbs.ChanStateDB.Backend,
)
if err != nil {
+ cleanUp()
err = fmt.Errorf("unable to get payments store: %w",
err)
@@ -1286,8 +1290,12 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
// Check if the invoice bucket tombstone is set. If it is, we
// need to return and ask the user switch back to using the
// native SQL store.
+ //
+ // NOTE: The invoice bucket tombstone acts as the system-wide
+ // guard against switching back to KV mode.
ripInvoices, err := dbs.ChanStateDB.GetInvoiceBucketTombstone()
if err != nil {
+ cleanUp()
err = fmt.Errorf("unable to check invoice bucket "+
"tombstone: %w", err)
d.logger.Error(err)
@@ -1295,6 +1303,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
return nil, nil, err
}
if ripInvoices {
+ cleanUp()
err = fmt.Errorf("invoices bucket tombstoned, please " +
"switch back to native SQL")
d.logger.Error(err)
@@ -1302,33 +1311,14 @@ 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(
databaseBackends.GraphDB, graphDBOptions...,
)
if err != nil {
+ cleanUp()
+
return nil, nil, err
}
diff --git a/payments/db/kv_tombstone.go b/payments/db/kv_tombstone.go
deleted file mode 100644
index 7dc13b9..0000000
--- a/payments/db/kv_tombstone.go
+++ /dev/null
@@ -1,71 +0,0 @@
-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
-}
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.