What changed, and why it matters
This patch fixes a database migration bug in LND's channel database. Some databases were created without recording their schema version number, which could cause future mandatory upgrades to be skipped. The fix detects that missing version marker, safely resumes upgrades from a known baseline, and ensures a specific v0.21 data-format migration (for 'waiting proof' records) actually runs. It is a correctness/reliability fix rather than a remote attack vector, but skipped migrations can leave data in an inconsistent state that may affect node behavior.
Apply the patch in the 0.21.2 release. Operators who initialized an LND node on affected v0.20.x code should upgrade so the waiting-proof migration runs and the DB version key is written. Monitor logs for the recovery warning. No immediate external mitigation is required because exploitation requires prior local DB state.
Security signals we found
Database schema version key can be absent due to an init-ordering regression
Missing version key previously caused migration selection to treat DB as latest, potentially skipping mandatory migrations
Fix recovers baseline from last mandatory version before the regression (33)
Specifically ensures waiting proof migration (35) runs on affected databases
Guard prevents unsafe inference on databases older than version 33
Evidence from the diff
In channeldb/db.go, syncVersions now distinguishes ErrMetaNotFound from ErrDBVersionNotFound. If the metadata bucket exists but the dbVersionKey is absent, and the compiled migration list includes at least version 33, the code sets Meta.DbVersionNumber to 33 (missingDBVersionRecoveryVersion) instead of treating the DB as latest. This lets migration 35 (waiting proof format change) run without replaying migrations 0-33 against a DB already created by modern code. A guard returns an error if the version list is too old to justify the inference. Tests verify recovery runs migration 35 and that recovery is rejected when the baseline is absent. The release notes describe this as a fixed channeldb migration bug.
Changed components
lnd/channeldb/db.golnd/channeldb/meta_test.godocs/release-notes/release-notes-0.21.2.mdInspect captured patch +176 / −3
diff --git a/channeldb/db.go b/channeldb/db.go
index d801de6..3ed623e 100644
--- a/channeldb/db.go
+++ b/channeldb/db.go
@@ -43,6 +43,14 @@ import (
const (
dbName = "channel.db"
+
+ // missingDBVersionRecoveryVersion is the latest mandatory DB
+ // version before the init ordering regression that could create a
+ // DB without writing the DB version key. Affected DBs are therefore
+ // already at least this version, so recovery starts here to run the
+ // v0.21 waiting proof migration without replaying older migrations
+ // against a modern DB.
+ missingDBVersionRecoveryVersion = 33
)
var (
@@ -1869,16 +1877,43 @@ func (c *ChannelStateDB) DeleteChannelOpeningState(outPoint []byte) error {
// applies migration functions to the current database and recovers the
// previous state of db if at least one error/panic appeared during migration.
func (d *DB) syncVersions(versions []mandatoryVersion) error {
+ latestVersion := getLatestDBVersion(versions)
+
meta, err := d.FetchMeta()
if err != nil {
- if err == ErrMetaNotFound {
+ switch {
+ case errors.Is(err, ErrMetaNotFound):
meta = &Meta{}
- } else {
+
+ case errors.Is(err, ErrDBVersionNotFound):
+ recoveryVersion := uint32(
+ missingDBVersionRecoveryVersion,
+ )
+
+ // Missing DB version recovery is only valid for DBs
+ // created after the init ordering regression. Older DBs
+ // wrote the DB version before init returned, so a
+ // missing version key on a sub-33 DB is not a valid
+ // state to infer from.
+ if latestVersion < recoveryVersion {
+ return fmt.Errorf("unable to recover missing "+
+ "DB version key: latest_version=%v "+
+ "recovery_version=%v", latestVersion,
+ recoveryVersion)
+ }
+
+ log.Warnf("DB version key missing, recovering from "+
+ "db_version=%v", recoveryVersion)
+
+ meta = &Meta{
+ DbVersionNumber: recoveryVersion,
+ }
+
+ default:
return err
}
}
- latestVersion := getLatestDBVersion(versions)
log.Infof("Checking for schema update: latest_version=%v, "+
"db_version=%v", latestVersion, meta.DbVersionNumber)
diff --git a/channeldb/meta_test.go b/channeldb/meta_test.go
index ca25328..a7a0b4f 100644
--- a/channeldb/meta_test.go
+++ b/channeldb/meta_test.go
@@ -2,12 +2,14 @@ package channeldb
import (
"bytes"
+ "encoding/binary"
"errors"
"fmt"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/kvdb"
+ "github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
@@ -687,6 +689,135 @@ func TestInitChannelDBCreatesMissingTopLevelBuckets(t *testing.T) {
require.NoError(t, err)
}
+// TestMissingDBVersionRunsWaitingProofMigration asserts that a DB initialized
+// without a version key is recovered from the last v0.20 mandatory version so
+// migration 35 can migrate legacy waiting proof records.
+func TestMissingDBVersionRunsWaitingProofMigration(t *testing.T) {
+ t.Parallel()
+
+ backend, cleanup, err := kvdb.GetTestBackend(t.TempDir(), "cdb")
+ require.NoError(t, err)
+ t.Cleanup(cleanup)
+
+ const scid = 101
+ ann := &lnwire.AnnounceSignatures1{
+ ChannelID: lnwire.ChannelID{1, 2, 3},
+ ShortChannelID: lnwire.NewShortChanIDFromInt(scid),
+ NodeSignature: wireSig,
+ BitcoinSignature: wireSig,
+ ExtraOpaqueData: []byte{4, 5, 6},
+ }
+
+ legacyKey, legacyValue := encodeLegacyWaitingProof(t, true, ann)
+
+ err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
+ _, err := tx.CreateTopLevelBucket(metaBucket)
+ if err != nil {
+ return err
+ }
+
+ bucket, err := tx.CreateTopLevelBucket(waitingProofsBucketKey)
+ if err != nil {
+ return err
+ }
+
+ return bucket.Put(legacyKey[:], legacyValue)
+ }, func() {})
+ require.NoError(t, err)
+
+ db, err := CreateWithBackend(backend)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, db.Close())
+ })
+
+ err = db.View(func(tx kvdb.RTx) 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),
+ )
+
+ bucket := tx.ReadBucket(waitingProofsBucketKey)
+ require.NotNil(t, bucket)
+ require.Nil(t, bucket.Get(legacyKey[:]))
+
+ proof := NewWaitingProof(true, ann)
+ typedKey := proof.Key()
+ require.NotNil(t, bucket.Get(typedKey[:]))
+
+ return nil
+ }, func() {})
+ require.NoError(t, err)
+
+ store, err := NewWaitingProofStore(db)
+ require.NoError(t, err)
+
+ proof := NewWaitingProof(true, ann)
+ migratedProof, err := store.Get(proof.Key())
+ require.NoError(t, err)
+ require.Equal(t, proof.Key(), migratedProof.Key())
+}
+
+// TestMissingDBVersionRecoveryRequiresBaseline asserts that a missing version
+// key cannot be recovered if the target version list does not include the
+// recovery baseline.
+func TestMissingDBVersionRecoveryRequiresBaseline(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,
+ }
+
+ versions := []mandatoryVersion{
+ {
+ number: 0,
+ migration: nil,
+ },
+ {
+ number: 1,
+ migration: nil,
+ },
+ }
+
+ err = db.syncVersions(versions)
+ require.ErrorContains(t, err, "unable to recover missing DB version")
+}
+
+// encodeLegacyWaitingProof encodes a waiting proof using the pre-migration
+// format.
+func encodeLegacyWaitingProof(t *testing.T, isRemote bool,
+ ann *lnwire.AnnounceSignatures1) ([9]byte, []byte) {
+
+ t.Helper()
+
+ var key [9]byte
+ binary.BigEndian.PutUint64(key[:8], ann.ShortChannelID.ToUint64())
+ if isRemote {
+ key[8] = 1
+ }
+
+ var value bytes.Buffer
+ require.NoError(t, binary.Write(&value, byteOrder, isRemote))
+ require.NoError(t, ann.Encode(&value, 0))
+
+ return key, value.Bytes()
+}
+
// TestMarkerAndTombstone tests that markers like a tombstone can be added to a
// DB.
func TestMarkerAndTombstone(t *testing.T) {
diff --git a/docs/release-notes/release-notes-0.21.2.md b/docs/release-notes/release-notes-0.21.2.md
index ccf4822..d4bee4f 100644
--- a/docs/release-notes/release-notes-0.21.2.md
+++ b/docs/release-notes/release-notes-0.21.2.md
@@ -36,6 +36,13 @@
combination. This also affects callers replaying an affected historical
route returned by `ListPayments` or `TrackPayment`.
+* [Fixed a channeldb migration
+ bug](https://github.com/lightningnetwork/lnd/pull/10985) where databases
+ initialized without a persisted `metadata/dbp` version key could skip later
+ mandatory migrations. This recovers such databases from the last known
+ v0.20-era mandatory version so the v0.21 waiting proof migration runs
+ without replaying older migrations against an already-initialized database.
+
# New Features
## Functional Enhancements
Why this scored 58/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.