channeldb: preserve db version during init

Keep the top-level bucket creation introduced by PR #9653, since initialized
DBs can still be missing newer buckets such as the historical channel bucket.

Do not let the metadata bucket created during init make a fresh DB look
initialized. Use strict metadata reads so a missing metadata/dbp key is distinct
from a present DB version, then write metadata/dbp for genuinely fresh DBs.

Existing DBs with a metadata bucket but missing metadata/dbp are left for the
migration recovery path instead of being treated as latest.
This commit is contained in:
Elle Mouton 2026-07-21 17:23:57 -07:00
parent 0f4b9c9a18
commit 619913bfb2
No known key found for this signature in database
GPG key ID: D7D916376026F177
4 changed files with 111 additions and 10 deletions

View file

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

View file

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

View file

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

View file

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