What changed, and why it matters
This commit fixes a database initialization bug in LND's channel database. Previously, creating an empty metadata bucket during setup could trick the code into thinking an old or partially-created database was already fully up-to-date, potentially skipping needed upgrades. The change distinguishes between a truly new database, a database missing its version key, and an already-initialized database, so migrations run when they should.
Review migration recovery paths to ensure they correctly handle databases with a metadata bucket but missing dbVersionKey. Verify that existing deployments cannot reach the problematic state in production, and consider backporting to affected release branches.
Security signals we found
Database version detection logic flaw
Potential migration skip due to incorrect initialized-state detection
New explicit error for missing DB version key
Added unit tests covering missing version key and missing top-level buckets
Evidence from the diff
The patch changes channeldb’s init and metadata reading logic. FetchMeta now returns a new ErrDBVersionNotFound error when the meta bucket exists but the dbVersionKey is absent, instead of silently setting the version to latest. initChannelDB now handles three cases: (1) metadata with a version exists -> just create missing top-level buckets and return; (2) no metadata bucket at all -> fresh DB, create buckets and write latest version; (3) metadata bucket exists but no version -> leave for migration recovery. This prevents a partially-initialized DB with only a metadata bucket from being classified as latest-version and skipping migrations.
Changed components
channeldb/db.gochanneldb/error.gochanneldb/meta.gochanneldb/meta_test.goInspect captured patch +111 / −10
diff --git a/channeldb/db.go b/channeldb/db.go
index a66e8f1..d801de6 100644
--- a/channeldb/db.go
+++ b/channeldb/db.go
@@ -502,21 +502,36 @@ func initChannelDB(db kvdb.Backend) error {
return err
}
+ meta := &Meta{}
+ metaErr := FetchMeta(meta, tx)
+
for _, tlb := range dbTopLevelBuckets {
if _, err := tx.CreateTopLevelBucket(tlb); err != nil {
return err
}
}
- meta := &Meta{}
- // Check if DB is already initialized.
- err := FetchMeta(meta, tx)
- if err == nil {
+ switch {
+ // Metadata with a DB version already exists. Required
+ // top-level buckets were created above, so init is complete.
+ case metaErr == nil:
+ return nil
+
+ // There is no metadata bucket at all, so this is a fresh DB.
+ // Initialize the DB version after creating the required
+ // top-level buckets.
+ case errors.Is(metaErr, ErrMetaNotFound):
+ meta.DbVersionNumber = getLatestDBVersion(dbVersions)
+ return putMeta(meta, tx)
+
+ // The DB already has a metadata bucket but no version key.
+ // Leave recovery to the migration path, which can infer a
+ // safe starting version before writing the version key.
+ case errors.Is(metaErr, ErrDBVersionNotFound):
return nil
}
- meta.DbVersionNumber = getLatestDBVersion(dbVersions)
- return putMeta(meta, tx)
+ return metaErr
}, func() {})
if err != nil {
return fmt.Errorf("unable to create new channeldb: %w", err)
diff --git a/channeldb/error.go b/channeldb/error.go
index c2b2dde..adadd2a 100644
--- a/channeldb/error.go
+++ b/channeldb/error.go
@@ -42,6 +42,10 @@ var (
// created.
ErrMetaNotFound = fmt.Errorf("unable to locate meta information")
+ // ErrDBVersionNotFound is returned when the meta bucket exists, but
+ // the DB version key hasn't been written.
+ ErrDBVersionNotFound = fmt.Errorf("unable to locate db version")
+
// ErrNoClosedChannels is returned when a node is queries for all the
// channels it has closed, but it hasn't yet closed any channels.
ErrNoClosedChannels = fmt.Errorf("no channel have been closed yet")
diff --git a/channeldb/meta.go b/channeldb/meta.go
index 127acf5..b23c880 100644
--- a/channeldb/meta.go
+++ b/channeldb/meta.go
@@ -46,7 +46,7 @@ type Meta struct {
// FetchMeta fetches the metadata from boltdb and returns filled meta structure.
func (d *DB) FetchMeta() (*Meta, error) {
- var meta *Meta
+ meta := &Meta{}
err := kvdb.View(d, func(tx kvdb.RTx) error {
return FetchMeta(meta, tx)
@@ -70,11 +70,11 @@ func FetchMeta(meta *Meta, tx kvdb.RTx) error {
data := metaBucket.Get(dbVersionKey)
if data == nil {
- meta.DbVersionNumber = getLatestDBVersion(dbVersions)
- } else {
- meta.DbVersionNumber = byteOrder.Uint32(data)
+ return ErrDBVersionNotFound
}
+ meta.DbVersionNumber = byteOrder.Uint32(data)
+
return nil
}
diff --git a/channeldb/meta_test.go b/channeldb/meta_test.go
index ea314bc..ca25328 100644
--- a/channeldb/meta_test.go
+++ b/channeldb/meta_test.go
@@ -603,6 +603,88 @@ func TestFetchMeta(t *testing.T) {
require.NoError(t, err)
require.Equal(t, LatestDBVersion(), meta.DbVersionNumber)
+
+ err = db.View(func(tx walletdb.ReadTx) error {
+ metaBucket := tx.ReadBucket(metaBucket)
+ require.NotNil(t, metaBucket)
+
+ versionBytes := metaBucket.Get(dbVersionKey)
+ require.Len(t, versionBytes, 4)
+ require.Equal(
+ t, LatestDBVersion(), byteOrder.Uint32(versionBytes),
+ )
+
+ return nil
+ }, func() {})
+ require.NoError(t, err)
+}
+
+// TestFetchMetaMissingDBVersion asserts that metadata with no DB version key is
+// reported as incomplete metadata.
+func TestFetchMetaMissingDBVersion(t *testing.T) {
+ t.Parallel()
+
+ backend, cleanup, err := kvdb.GetTestBackend(t.TempDir(), "cdb")
+ require.NoError(t, err)
+ t.Cleanup(cleanup)
+
+ err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
+ _, err := tx.CreateTopLevelBucket(metaBucket)
+
+ return err
+ }, func() {})
+ require.NoError(t, err)
+
+ db := &DB{
+ Backend: backend,
+ }
+
+ _, err = db.FetchMeta()
+ require.ErrorIs(t, err, ErrDBVersionNotFound)
+
+ err = kvdb.View(backend, func(tx kvdb.RTx) error {
+ meta := &Meta{}
+ err := FetchMeta(meta, tx)
+ require.ErrorIs(t, err, ErrDBVersionNotFound)
+
+ return nil
+ }, func() {})
+ require.NoError(t, err)
+}
+
+// TestInitChannelDBCreatesMissingTopLevelBuckets asserts that initialized DBs
+// with missing top-level buckets are repaired during initialization.
+func TestInitChannelDBCreatesMissingTopLevelBuckets(t *testing.T) {
+ t.Parallel()
+
+ backend, cleanup, err := kvdb.GetTestBackend(t.TempDir(), "cdb")
+ require.NoError(t, err)
+ t.Cleanup(cleanup)
+
+ err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
+ meta := &Meta{
+ DbVersionNumber: LatestDBVersion(),
+ }
+
+ return putMeta(meta, tx)
+ }, func() {})
+ require.NoError(t, err)
+
+ err = kvdb.View(backend, func(tx kvdb.RTx) error {
+ require.Nil(t, tx.ReadBucket(historicalChannelBucket))
+
+ return nil
+ }, func() {})
+ require.NoError(t, err)
+
+ require.NoError(t, initChannelDB(backend))
+
+ err = kvdb.View(backend, func(tx kvdb.RTx) error {
+ require.NotNil(t, tx.ReadBucket(historicalChannelBucket))
+
+ return nil
+ }, func() {})
+ require.NoError(t, err)
}
// TestMarkerAndTombstone tests that markers like a tombstone can be added to a
Why this scored 54/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.