diff --git a/channeldb/db.go b/channeldb/db.go index a66e8f19f..d801de691 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 c2b2dde0d..adadd2ae6 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 127acf51f..b23c88014 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 ea314bc98..ca25328c1 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