channeldb: recover missing db version

Use strict metadata reads during migration selection so a metadata bucket with a
missing metadata/dbp key is not interpreted as the latest DB version.

Recover this state from mandatory DB version 33, the last mandatory version
before the v0.20.x releases that could initialize a DB without writing the DB
version key. This runs migration 35 without replaying migrations 0 through 33
against a DB that was already created by a modern schema/code path.

After the selected migrations complete, syncVersions writes the latest DB
version as usual.
This commit is contained in:
Elle Mouton 2026-07-21 17:26:47 -07:00
parent 3aff61aebe
commit 68264c70d9
No known key found for this signature in database
GPG key ID: D7D916376026F177
3 changed files with 176 additions and 3 deletions

View file

@ -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)

View file

@ -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) {

View file

@ -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